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 GUILD_CONTRACT_BOARD = preload("res://assets/entry/guild_props/contract_board.png") const GUILD_ROUTE_MAP = preload("res://assets/entry/guild_props/route_map.png") 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 PartyLineTrack = preload("res://entry/party_line_track.gd") const PartyLineDiagram = preload("res://entry/party_line_diagram.gd") const BattleService = preload("res://entry/battle_service.gd") const BattleOpeningArt = preload("res://entry/battle_opening_art.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 CONTENT_BACKGROUND = preload("res://assets/backgrounds/first_expedition_field.png") const TITLE_MASTER_V002 = preload("res://assets/backgrounds/title_master_v002.png") const BOOT_BACKGROUND = preload("res://assets/entry/boot/background.png") const BOOT_IDENTITY_PLAQUE = preload("res://assets/entry/boot/identity-plaque.png") const BOOT_VERIFICATION_RAIL = preload("res://assets/entry/boot/verification-rail.png") const BOOT_STATUS_SEAL = preload("res://assets/entry/boot/status-seal.png") const BOOT_ACTION_TAB = preload("res://assets/entry/boot/action-tab.png") const FIRST_LAUNCH_BACKGROUND = preload("res://assets/entry/first_launch/background.png") const FIRST_LAUNCH_PREVIEW_FRAME = preload("res://assets/entry/first_launch/preview-frame.png") const FIRST_LAUNCH_SETTING_ROW = preload("res://assets/entry/first_launch/setting-row.png") const FIRST_LAUNCH_APPLY_COMMAND = preload("res://assets/entry/first_launch/apply-command.png") const FIRST_LAUNCH_RETURN_TAB = preload("res://assets/entry/first_launch/return-tab.png") const FIRST_LAUNCH_SETTING_ICON = preload("res://assets/entry/first_launch/setting-icon.png") const FIRST_LAUNCH_VALUE_PLATE = preload("res://assets/entry/first_launch/value-plate.png") const GUILD_MASTER_BACKGROUND = preload("res://assets/backgrounds/guild_master_b_background.png") const GUILD_MASTER_COMPOSITE = preload("res://assets/backgrounds/guild_master_b_composite.png") const GUILD_MASTER_BELLKEEPER = preload("res://assets/entry/guild_master_b_characters/company_bellkeeper.png") const GUILD_MASTER_SCOUT = preload("res://assets/entry/guild_master_b_characters/company_scout.png") const GUILD_MASTER_TOP_RAIL = preload("res://assets/entry/guild_master_b_ui/top_rail.png") const GUILD_MASTER_BOTTOM_RAIL = preload("res://assets/entry/guild_master_b_ui/bottom_rail.png") const GUILD_MASTER_KIOSK = preload("res://assets/entry/guild_master_b_ui/contract_kiosk.png") const GUILD_MASTER_COMPASS = preload("res://assets/entry/guild_master_b_ui/route_compass.png") const GUILD_MASTER_MEDALLION = preload("res://assets/entry/guild_master_b_ui/primary_medallion.png") const GUILD_DESK_WORLD = preload("res://assets/entry/guild_desk_v001/world.png") const GUILD_DESK_CONTRACT_BOARD = preload("res://assets/entry/guild_desk_v001/contract-board.png") const GUILD_DESK_RESOURCE_BADGE = preload("res://assets/entry/guild_desk_v001/resource-badge.png") const GUILD_DESK_TOP_LEFT = preload("res://assets/entry/guild_desk_v001/top_rail/left.png") const GUILD_DESK_TOP_CENTER = preload("res://assets/entry/guild_desk_v001/top_rail/center.png") const GUILD_DESK_TOP_RIGHT = preload("res://assets/entry/guild_desk_v001/top_rail/right.png") const GUILD_DESK_TOP_PRESSURE = preload("res://assets/entry/guild_desk_v001/top_rail/pressure.png") const GUILD_DESK_TOP_COIN = preload("res://assets/entry/guild_desk_v001/top_rail/coin.png") const GUILD_DESK_TOP_COMPANY = preload("res://assets/entry/guild_desk_v001/top_rail/company.png") const GUILD_DESK_BOTTOM_LEFT = preload("res://assets/entry/guild_desk_v001/bottom_rail/left.png") const GUILD_DESK_BOTTOM_CENTER = preload("res://assets/entry/guild_desk_v001/bottom_rail/center.png") const GUILD_DESK_BOTTOM_RIGHT = preload("res://assets/entry/guild_desk_v001/bottom_rail/right.png") const GUILD_DESK_BOTTOM_ROSTER = preload("res://assets/entry/guild_desk_v001/bottom_rail/roster.png") const GUILD_DESK_BOTTOM_EQUIPMENT = preload("res://assets/entry/guild_desk_v001/bottom_rail/equipment.png") const GUILD_DESK_BOTTOM_PARTY = preload("res://assets/entry/guild_desk_v001/bottom_rail/party.png") const PARTY_COMMAND_UNDO = preload("res://assets/entry/party_line_v001/undo.png") const PARTY_COMMAND_RESET = preload("res://assets/entry/party_line_v001/reset.png") const PARTY_COMMAND_SAVE = preload("res://assets/entry/party_line_v001/save-order.png") const PARTY_COMMAND_BACK = preload("res://assets/entry/party_line_v001/back.png") const PARTY_LINE_MASTER_PATH := "res://assets/entry/party_line_v001/party-line-master.png" const READINESS_STATIC_MASTER_PATH := "res://assets/entry/pty_002_readiness/readiness-static-master.png" const READINESS_COMMAND_REVISE = preload("res://assets/entry/pty_002_readiness/revise_party@3x.png") const READINESS_COMMAND_BACK = preload("res://assets/entry/pty_002_readiness/back_to_guild@3x.png") const READINESS_COMMAND_ACCEPT = preload("res://assets/entry/pty_002_readiness/accept_preparation@3x.png") const READINESS_COMMAND_RISK = preload("res://assets/entry/pty_002_readiness/risk_next@3x.png") const RISK_BOARD_STATIC_MASTER = preload("res://assets/entry/rsk_001_risk_board/risk-board-static-master.png") const RISK_COMMITMENT_STATIC_MASTER = preload("res://assets/entry/rsk_002_risk_commitment/risk-commitment-static-master.png") const BATTLE_OPENING_BACKGROUND = preload("res://assets/backgrounds/first_expedition_field.png") const BATTLE_OPENING_ENEMY = preload("res://assets/enemies/riftblade/idle.png") const BATTLE_OPENING_HEARTHGUARD = preload("res://assets/actors/hearthguard.png") const BATTLE_OPENING_WAYRUNNER = preload("res://assets/actors/wayrunner.png") const BATTLE_OPENING_FARSTRING = preload("res://assets/actors/farstring.png") const BATTLE_OPENING_KINDHAND = preload("res://assets/actors/kindhand.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 boot_diagnostics_button: Button var boot_exit_button: Button var boot_responsive_layout: Control var boot_identity_plaque: TextureRect var boot_verification_rail: NinePatchRect var boot_status_seals: Array[TextureRect] = [] var boot_status_marks: Array[Label] = [] var boot_progress_value: Label var boot_diagnostics_expanded := false var first_launch_responsive_layout: Control var first_launch_preview_frame: NinePatchRect var first_launch_preview_image: TextureRect var first_launch_preview_title_plate: NinePatchRect var first_launch_preview_detail_plate: NinePatchRect var first_launch_settings_panel: Control var first_launch_apply: Button var first_launch_error: Label var first_launch_setting_buttons: Dictionary = {} var first_launch_setting_values: Dictionary = {} 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 risk_commitment_acknowledged := false var risk_commitment_bound_risk_id := "" var risk_commitment_bound_snapshot_hash := "" var risk_commitment_bound_option_hash := "" var risk_commitment_bound_contract_hash := "" var risk_commitment_notice := "" var risk_commitment_receipt: Dictionary = {} 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 var battle_opening_mode := false var battle_opening_state := "ready_to_observe" var battle_opening_notice := "" var battle_opening_handoff_receipt: Dictionary = {} var battle_opening_canvas: Control var title_master_design_canvas: Control var guild_master_design_canvas: Control var party_master_design_canvas: Control var readiness_master_design_canvas: Control var risk_board_master_design_canvas: Control var risk_commitment_master_design_canvas: Control func _notification(what: int) -> void: if what == NOTIFICATION_RESIZED and is_instance_valid(boot_responsive_layout): _layout_boot_screen() if what == NOTIFICATION_RESIZED and is_instance_valid(first_launch_responsive_layout): _layout_first_launch() if what == NOTIFICATION_RESIZED and is_instance_valid(title_master_design_canvas): _layout_title_master_canvas() if what == NOTIFICATION_RESIZED and is_instance_valid(guild_master_design_canvas): _layout_guild_master_canvas() if what == NOTIFICATION_RESIZED and is_instance_valid(party_master_design_canvas): _layout_party_master_canvas() if what == NOTIFICATION_RESIZED and is_instance_valid(readiness_master_design_canvas): _layout_readiness_master_canvas() if what == NOTIFICATION_RESIZED and is_instance_valid(risk_board_master_design_canvas): _layout_risk_board_master_canvas() if what == NOTIFICATION_RESIZED and is_instance_valid(risk_commitment_master_design_canvas): _layout_risk_commitment_master_canvas() if what == NOTIFICATION_RESIZED and is_instance_valid(battle_opening_canvas): _layout_battle_opening_canvas() 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 boot_diagnostics_expanded = false 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 show_boot_diagnostics() -> void: if String(boot_snapshot.get("state", "")) != "error": return boot_diagnostics_expanded = true diagnostics_label.visible = true _layout_boot_screen() func exit_from_boot() -> void: get_tree().quit() func _build_boot_screen() -> void: current_screen = "boot" for child in get_children(): child.free() boot_status_seals.clear() boot_status_marks.clear() var background := TextureRect.new() background.name = "BootWorld" background.texture = BOOT_BACKGROUND background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED background.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS 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 = "BootWorldShade" shade.color = Color(0.035, 0.07, 0.10, 0.06) shade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) shade.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(shade) boot_responsive_layout = Control.new() boot_responsive_layout.name = "BootResponsiveLayout" boot_responsive_layout.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) boot_responsive_layout.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(boot_responsive_layout) boot_identity_plaque = TextureRect.new() boot_identity_plaque.name = "BootIdentityPlaque" boot_identity_plaque.texture = BOOT_IDENTITY_PLAQUE boot_identity_plaque.expand_mode = TextureRect.EXPAND_IGNORE_SIZE boot_identity_plaque.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED boot_identity_plaque.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS boot_identity_plaque.mouse_filter = Control.MOUSE_FILTER_IGNORE boot_responsive_layout.add_child(boot_identity_plaque) var identity := Label.new() identity.name = "BootIdentity" identity.text = _tr("裂脉远征团", "AETHERBOUND GUILD") identity.add_theme_font_override("font", UI_FONT) identity.add_theme_font_size_override("font_size", _scaled(18)) identity.add_theme_color_override("font_color", Color("#213744")) identity.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER identity.vertical_alignment = VERTICAL_ALIGNMENT_CENTER identity.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART identity.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS identity.mouse_filter = Control.MOUSE_FILTER_IGNORE boot_identity_plaque.add_child(identity) boot_verification_rail = _boot_nine_patch("BootVerificationRail", BOOT_VERIFICATION_RAIL, 112, 0) boot_responsive_layout.add_child(boot_verification_rail) for index in range(3): var seal := TextureRect.new() seal.name = "BootStatusSeal%d" % index seal.texture = BOOT_STATUS_SEAL seal.expand_mode = TextureRect.EXPAND_IGNORE_SIZE seal.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED seal.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS seal.mouse_filter = Control.MOUSE_FILTER_IGNORE boot_verification_rail.add_child(seal) boot_status_seals.append(seal) var mark := _label(str(index + 1), _scaled(13), Color("#f5ead0")) mark.name = "BootStatusMark%d" % index mark.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) mark.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER mark.vertical_alignment = VERTICAL_ALIGNMENT_CENTER mark.mouse_filter = Control.MOUSE_FILTER_IGNORE seal.add_child(mark) boot_status_marks.append(mark) 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", _scaled(15)) status_label.add_theme_color_override("font_color", Color("#213744")) status_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS status_label.mouse_filter = Control.MOUSE_FILTER_IGNORE boot_verification_rail.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", _scaled(10)) detail_label.add_theme_color_override("font_color", Color("#5d5a50")) detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail_label.mouse_filter = Control.MOUSE_FILTER_IGNORE boot_verification_rail.add_child(detail_label) progress_bar = ProgressBar.new() progress_bar.name = "BootProgress" progress_bar.show_percentage = false progress_bar.add_theme_stylebox_override("background", _flat_style(Color("#d7c6a6"), Color("#866f47"), 1, 5)) progress_bar.add_theme_stylebox_override("fill", _flat_style(Color("#5dc9e9"), Color("#ead49b"), 1, 5)) boot_verification_rail.add_child(progress_bar) boot_progress_value = _label("0%", _scaled(9), Color("#213744")) boot_progress_value.name = "BootProgressValue" boot_progress_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT boot_progress_value.vertical_alignment = VERTICAL_ALIGNMENT_CENTER boot_progress_value.mouse_filter = Control.MOUSE_FILTER_IGNORE boot_verification_rail.add_child(boot_progress_value) retry_button = _boot_action_button("BootRetry", _tr("重试", "Retry"), retry_boot) retry_button.visible = false boot_verification_rail.add_child(retry_button) boot_diagnostics_button = _boot_action_button("BootDiagnosticsAction", _tr("诊断", "Diagnostics"), show_boot_diagnostics) boot_diagnostics_button.visible = false boot_verification_rail.add_child(boot_diagnostics_button) boot_exit_button = _boot_action_button("BootExit", _tr("退出", "Exit"), exit_from_boot) boot_exit_button.visible = false boot_verification_rail.add_child(boot_exit_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", _scaled(9)) diagnostics_label.add_theme_color_override("font_color", Color("#914b32")) diagnostics_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART diagnostics_label.mouse_filter = Control.MOUSE_FILTER_IGNORE boot_verification_rail.add_child(diagnostics_label) _layout_boot_screen() func _layout_boot_screen() -> void: if not is_instance_valid(boot_responsive_layout) or not is_instance_valid(boot_verification_rail): return var available := size if available.x <= 0.0 or available.y <= 0.0: return var safe_x := clampf(available.x * 0.04, 20.0, 48.0) var safe_y := clampf(available.y * 0.06, 16.0, 40.0) var plaque_width := clampf(available.x * 0.26, 180.0, 320.0) var plaque_height := clampf(plaque_width / 1.88, 72.0, 150.0) boot_identity_plaque.position = Vector2(safe_x, safe_y) boot_identity_plaque.size = Vector2(plaque_width, plaque_height) var identity := boot_identity_plaque.get_node_or_null("BootIdentity") as Label if identity != null: identity.position = Vector2(plaque_width * 0.14, plaque_height * 0.26) identity.size = Vector2(plaque_width * 0.72, plaque_height * 0.48) var rail_width := minf(available.x - safe_x * 2.0, minf(1120.0, available.x * 0.84)) var rail_height := clampf(available.y * 0.25, 96.0, 176.0) boot_verification_rail.position = Vector2((available.x - rail_width) * 0.5, available.y - safe_y - rail_height) boot_verification_rail.size = Vector2(rail_width, rail_height) var padding := clampf(rail_height * 0.12, 10.0, 18.0) var content_top := clampf(rail_height * 0.28, 27.0, 50.0) var content_bottom := clampf(rail_height * 0.17, 16.0, 30.0) var error_state := String(boot_snapshot.get("state", "running")) == "error" var seal_size := clampf(rail_height * 0.42, 36.0, 64.0) var seal_gap := clampf(seal_size * 0.12, 4.0, 8.0) var seals_width := seal_size * 3.0 + seal_gap * 2.0 for index in range(boot_status_seals.size()): var seal := boot_status_seals[index] seal.visible = not error_state seal.position = Vector2(padding + index * (seal_size + seal_gap), (rail_height - seal_size) * 0.5) seal.size = Vector2(seal_size, seal_size) if error_state: var inner_left := clampf(rail_height * 0.9, 84.0, 120.0) var inner_right := inner_left var inner_width := rail_width - inner_left - inner_right var copy_width := inner_width * 0.42 status_label.position = Vector2(inner_left, content_top) status_label.size = Vector2(copy_width, 26) detail_label.position = Vector2(inner_left, content_top + 27.0) detail_label.size = Vector2(copy_width, maxf(18.0, rail_height - content_top - content_bottom - 50.0)) diagnostics_label.position = Vector2(inner_left, rail_height - content_bottom - 23.0) diagnostics_label.size = Vector2(copy_width, 23.0) progress_bar.visible = false boot_progress_value.visible = false var action_x := inner_left + copy_width + padding var action_width := rail_width - inner_right - action_x var visible_actions: Array[Button] = [] for action in [retry_button, boot_diagnostics_button, boot_exit_button]: if action.visible: visible_actions.append(action) var action_gap := 6.0 var button_width := (action_width - action_gap * maxf(0.0, visible_actions.size() - 1.0)) / maxf(1.0, visible_actions.size()) for index in range(visible_actions.size()): visible_actions[index].position = Vector2(action_x + index * (button_width + action_gap), (rail_height - 48.0) * 0.5) visible_actions[index].size = Vector2(button_width, 48.0) else: var copy_x := padding + seals_width + maxf(10.0, padding * 0.8) var copy_width := rail_width - copy_x - padding status_label.position = Vector2(copy_x, content_top) status_label.size = Vector2(copy_width, 24.0) detail_label.position = Vector2(copy_x, content_top + 24.0) detail_label.size = Vector2(copy_width, maxf(18.0, rail_height - content_top - content_bottom - 56.0)) var progress_y := rail_height - content_bottom - 16.0 progress_bar.position = Vector2(copy_x, progress_y) progress_bar.size = Vector2(maxf(80.0, copy_width - 54.0), 12.0) progress_bar.visible = true boot_progress_value.position = Vector2(copy_x + copy_width - 50.0, progress_y - 5.0) boot_progress_value.size = Vector2(50.0, 22.0) boot_progress_value.visible = true diagnostics_label.visible = false func _boot_nine_patch(node_name: String, texture: Texture2D, horizontal_margin: int, vertical_margin: int) -> NinePatchRect: var panel := NinePatchRect.new() panel.name = node_name panel.texture = texture panel.patch_margin_left = horizontal_margin panel.patch_margin_right = horizontal_margin panel.patch_margin_top = vertical_margin panel.patch_margin_bottom = vertical_margin panel.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS panel.mouse_filter = Control.MOUSE_FILTER_IGNORE return panel func _boot_action_button(node_name: String, text_value: String, callback: Callable) -> Button: var button := Button.new() button.name = node_name button.text = text_value button.custom_minimum_size = Vector2(72.0, 48.0) button.add_theme_font_override("font", UI_FONT) button.add_theme_font_size_override("font_size", _scaled(10)) button.add_theme_color_override("font_color", Color("#f5ead0")) button.add_theme_color_override("font_hover_color", Color.WHITE) button.add_theme_stylebox_override("normal", _boot_texture_style(Color.WHITE)) button.add_theme_stylebox_override("hover", _boot_texture_style(Color("#d9f4ff"))) button.add_theme_stylebox_override("pressed", _boot_texture_style(Color("#b7cfdd"))) button.add_theme_stylebox_override("focus", _boot_texture_style(Color("#f4dfa5"))) button.pressed.connect(callback) return button func _boot_texture_style(modulate: Color) -> StyleBoxTexture: var style := StyleBoxTexture.new() style.texture = BOOT_ACTION_TAB style.texture_margin_left = 20.0 style.texture_margin_right = 20.0 style.texture_margin_top = 8.0 style.texture_margin_bottom = 8.0 style.modulate_color = modulate return style func _render_boot_state() -> void: if status_label == null: return var identity := boot_identity_plaque.get_node_or_null("BootIdentity") as Label if identity != null: identity.text = _tr("裂脉远征团", "AETHERBOUND GUILD") retry_button.text = _tr("重试", "Retry") boot_diagnostics_button.text = _tr("诊断", "Diagnostics") boot_exit_button.text = _tr("退出", "Exit") var state := String(boot_snapshot.get("state", "running")) var step := String(boot_snapshot.get("step", "resources")) var total: int = maxi(1, int(boot_snapshot.get("total", 1))) var completed := int(boot_snapshot.get("completed", 0)) progress_bar.max_value = total progress_bar.value = completed var percent := int(round(float(completed) / float(total) * 100.0)) boot_progress_value.text = "%d%%" % percent var error_code := String(boot_snapshot.get("code", "UNKNOWN")) var recoverable := state == "error" and _boot_error_is_recoverable(error_code) retry_button.visible = recoverable boot_diagnostics_button.visible = state == "error" boot_exit_button.visible = state == "error" diagnostics_label.visible = state == "error" and boot_diagnostics_expanded if state != "error": boot_diagnostics_expanded = false var complete_seals := 3 if state == "complete" else clampi(int(floor(float(completed) / float(total) * 3.0)), 0, 3) for index in range(boot_status_seals.size()): var finished := index < complete_seals var active := index == complete_seals and state == "running" boot_status_seals[index].modulate = Color.WHITE if finished or active else Color(0.62, 0.67, 0.70, 0.88) boot_status_marks[index].text = "OK" if finished else "..." if active else str(index + 1) boot_status_marks[index].add_theme_color_override("font_color", Color("#9ee9ff") if finished else Color("#f5ead0")) match state: "error": status_label.text = _tr("本地检查需要处理", "Verification needs attention") detail_label.text = _tr("本地数据保持不变。可重试的检查会显示重试操作;不安全的存档会保持停止。", "Local data was left unchanged. Recoverable checks offer Retry; unsafe save data remains stopped.") diagnostics_label.text = "%s: %s" % [_tr("诊断", "Diagnostic"), error_code] "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 = "" _layout_boot_screen() func _boot_error_is_recoverable(code: String) -> bool: return code != "SAVE_REVIEW_REQUIRED" 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() for child in get_children(): child.free() first_launch_setting_buttons.clear() first_launch_setting_values.clear() var background := TextureRect.new() background.name = "FirstLaunchWorld" background.texture = FIRST_LAUNCH_BACKGROUND background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED background.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS 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 = "FirstLaunchShade" shade.color = Color(0.02, 0.05, 0.07, 0.09) shade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) shade.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(shade) first_launch_responsive_layout = Control.new() first_launch_responsive_layout.name = "FirstLaunchResponsiveLayout" first_launch_responsive_layout.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) first_launch_responsive_layout.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(first_launch_responsive_layout) first_launch_preview_frame = _first_nine_patch("FirstLaunchPreviewFrame", FIRST_LAUNCH_PREVIEW_FRAME, 86, 74) first_launch_responsive_layout.add_child(first_launch_preview_frame) first_launch_preview_image = TextureRect.new() first_launch_preview_image.name = "FirstLaunchPreviewImage" first_launch_preview_image.texture = FIRST_LAUNCH_BACKGROUND first_launch_preview_image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE first_launch_preview_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED first_launch_preview_image.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS first_launch_preview_image.mouse_filter = Control.MOUSE_FILTER_IGNORE first_launch_preview_frame.add_child(first_launch_preview_image) var preview_guide := Panel.new() preview_guide.name = "FirstLaunchSafeGuide" preview_guide.add_theme_stylebox_override("panel", _first_outline_style(Color("#e5f7ff"), 2, 0.82)) preview_guide.mouse_filter = Control.MOUSE_FILTER_IGNORE first_launch_preview_frame.add_child(preview_guide) var preview_heading := _label(_tr("即时预览", "LIVE PREVIEW"), _scaled(13), Color("#f5ead0")) preview_heading.name = "FirstLaunchPreviewHeading" preview_heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER preview_heading.mouse_filter = Control.MOUSE_FILTER_IGNORE first_launch_preview_frame.add_child(preview_heading) first_launch_preview_title_plate = _first_nine_patch("SetupPreviewTitlePlate", FIRST_LAUNCH_VALUE_PLATE, 72, 0) first_launch_preview_title_plate.mouse_filter = Control.MOUSE_FILTER_IGNORE first_launch_preview_frame.add_child(first_launch_preview_title_plate) var preview_title := _label(_tr("裂脉远征团", "AETHERBOUND GUILD"), _scaled(20), Color("#213744")) preview_title.name = "SetupPreviewTitle" preview_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER preview_title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER preview_title.mouse_filter = Control.MOUSE_FILTER_IGNORE first_launch_preview_frame.add_child(preview_title) first_launch_preview_detail_plate = _first_nine_patch("SetupPreviewDetailPlate", FIRST_LAUNCH_VALUE_PLATE, 72, 0) first_launch_preview_detail_plate.mouse_filter = Control.MOUSE_FILTER_IGNORE first_launch_preview_frame.add_child(first_launch_preview_detail_plate) var preview_detail := _label(_tr("安全区 · 字号 · 字幕", "Safe area · text · captions"), _scaled(10), Color("#3c403e")) preview_detail.name = "SetupPreviewDetail" preview_detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER preview_detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART preview_detail.clip_text = true preview_detail.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN preview_detail.size_flags_vertical = Control.SIZE_SHRINK_BEGIN preview_detail.mouse_filter = Control.MOUSE_FILTER_IGNORE first_launch_preview_frame.add_child(preview_detail) first_launch_settings_panel = Control.new() first_launch_settings_panel.name = "FirstLaunchControls" first_launch_settings_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE first_launch_responsive_layout.add_child(first_launch_settings_panel) var heading := _label(_tr("首次启动设置", "FIRST-LAUNCH SETUP"), _scaled(22), Color("#f5ead0")) heading.name = "FirstLaunchHeading" heading.add_theme_color_override("font_shadow_color", Color(0.02, 0.06, 0.08, 0.72)) heading.add_theme_constant_override("shadow_offset_x", 1) heading.add_theme_constant_override("shadow_offset_y", 2) first_launch_settings_panel.add_child(heading) var subheading := _label(_tr("这些选项只影响显示与辅助功能。", "These choices affect presentation and accessibility only."), _scaled(12), Color("#f5ead0")) subheading.name = "FirstLaunchSubheading" subheading.add_theme_color_override("font_shadow_color", Color(0.02, 0.06, 0.08, 0.68)) subheading.add_theme_constant_override("shadow_offset_x", 1) subheading.add_theme_constant_override("shadow_offset_y", 1) subheading.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART subheading.clip_text = true subheading.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN subheading.size_flags_vertical = Control.SIZE_SHRINK_BEGIN first_launch_settings_panel.add_child(subheading) var rows := [ {"id": "SetupLanguage", "label": ["语言", "Language"], "icon": "A", "kind": "choice", "callback": Callable(self, "toggle_setup_locale")}, {"id": "SetupSafeArea", "label": ["安全区", "Safe area"], "icon": "S", "kind": "choice", "callback": Callable(self, "cycle_setup_safe_area")}, {"id": "SetupTextScale", "label": ["字体", "Text scale"], "icon": "T", "kind": "choice", "callback": Callable(self, "cycle_setup_text_scale")}, {"id": "SetupReducedMotion", "label": ["减少动画", "Reduced motion"], "icon": "M", "kind": "toggle", "callback": Callable(self, "toggle_setup_reduced_motion")}, {"id": "SetupReducedFlashes", "label": ["减少闪烁", "Reduced flashes"], "icon": "F", "kind": "toggle", "callback": Callable(self, "toggle_setup_reduced_flashes")}, {"id": "SetupCaptions", "label": ["字幕与事件标签", "Captions"], "icon": "C", "kind": "toggle", "callback": Callable(self, "toggle_setup_captions")} ] for row_data in rows: var row := _first_setting_row(row_data) first_launch_settings_panel.add_child(row) var apply := _first_action_button("SetupApply", _tr("应用并进入标题首页", "Apply and continue to Title"), apply_first_launch, FIRST_LAUNCH_APPLY_COMMAND, 58) apply.focus_mode = Control.FOCUS_ALL first_launch_apply = apply first_launch_settings_panel.add_child(apply) first_launch_error = _label("", _scaled(11), Color("#914b32")) first_launch_error.name = "SetupError" first_launch_error.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART first_launch_settings_panel.add_child(first_launch_error) _layout_first_launch() _refresh_first_launch_labels() func _layout_first_launch() -> void: if not is_instance_valid(first_launch_responsive_layout) or not is_instance_valid(first_launch_preview_frame) or not is_instance_valid(first_launch_settings_panel): return var available := size if available.x <= 0.0 or available.y <= 0.0: return var safe_x := clampf(available.x * 0.035, 18.0, 48.0) var safe_y := clampf(available.y * 0.045, 14.0, 40.0) var gap := 16.0 if available.y >= 420.0 else 8.0 var usable_width := available.x - safe_x * 2.0 var settings_width := clampf(usable_width * (0.44 if available.x >= 900.0 else 0.48), 300.0, 520.0) var preview_width := maxf(270.0, usable_width - settings_width - gap) var content_height := available.y - safe_y * 2.0 var preview_height := maxf(190.0, content_height * (0.78 if available.y >= 420.0 else 0.70)) var preview_x := safe_x var preview_y := safe_y + (content_height - preview_height) * 0.38 first_launch_preview_frame.position = Vector2(preview_x, preview_y) first_launch_preview_frame.size = Vector2(preview_width, preview_height) first_launch_preview_image.position = Vector2(18.0, 18.0) first_launch_preview_image.size = Vector2(preview_width - 36.0, preview_height - 36.0) var guide := first_launch_preview_frame.get_node("FirstLaunchSafeGuide") as Control var safe_guide_inset := clampf(24.0 + float(setup_draft.safe_area) * 0.35, 24.0, minf(preview_width, preview_height) * 0.22) guide.position = Vector2(safe_guide_inset, safe_guide_inset) guide.size = Vector2(preview_width - safe_guide_inset * 2.0, preview_height - safe_guide_inset * 2.0) var preview_heading := first_launch_preview_frame.get_node("FirstLaunchPreviewHeading") as Label preview_heading.position = Vector2(26.0, 22.0) preview_heading.size = Vector2(preview_width - 52.0, 26.0) var preview_title := first_launch_preview_frame.get_node("SetupPreviewTitle") as Label var title_plate_rect := Rect2(preview_width * 0.15, preview_height * 0.37, preview_width * 0.70, 48.0) first_launch_preview_title_plate.position = title_plate_rect.position first_launch_preview_title_plate.size = title_plate_rect.size preview_title.position = title_plate_rect.position + Vector2(12.0, 3.0) preview_title.size = title_plate_rect.size - Vector2(24.0, 6.0) var preview_detail := first_launch_preview_frame.get_node("SetupPreviewDetail") as Label var detail_plate_rect := Rect2(preview_width * 0.12, preview_height - 73.0, preview_width * 0.76, 38.0) first_launch_preview_detail_plate.position = detail_plate_rect.position first_launch_preview_detail_plate.size = detail_plate_rect.size preview_detail.position = detail_plate_rect.position + Vector2(46.0, 2.0) preview_detail.size = detail_plate_rect.size - Vector2(92.0, 4.0) first_launch_settings_panel.position = Vector2(preview_x + preview_width + gap, safe_y) first_launch_settings_panel.size = Vector2(settings_width, content_height) var heading := first_launch_settings_panel.get_node("FirstLaunchHeading") as Label heading.position = Vector2(0, 0) heading.size = Vector2(settings_width, 32.0) var subheading := first_launch_settings_panel.get_node("FirstLaunchSubheading") as Label subheading.position = Vector2(0, 32.0) subheading.size = Vector2(settings_width, 30.0) var row_y := 68.0 var row_gap := 6.0 if available.y >= 420.0 else 4.0 var row_height := clampf((content_height - 150.0 - row_gap * 5.0) / 6.0, 48.0, 68.0) var row_names := ["SetupLanguage", "SetupSafeArea", "SetupTextScale", "SetupReducedMotion", "SetupReducedFlashes", "SetupCaptions"] var compact_columns := available.y < 400.0 if compact_columns: row_height = 48.0 var compact_width := (settings_width - row_gap) * 0.5 for index in range(row_names.size()): var compact_row := first_launch_setting_buttons[row_names[index]] as Button compact_row.position = Vector2((index % 2) * (compact_width + row_gap), row_y + floori(index / 2.0) * (row_height + row_gap)) compact_row.size = Vector2(compact_width, row_height) _layout_first_setting_row(compact_row) row_y += 3.0 * (row_height + row_gap) else: for row_data in row_names: var row := first_launch_setting_buttons[row_data] as Button row.position = Vector2(0, row_y) row.size = Vector2(settings_width, row_height) _layout_first_setting_row(row) row_y += row_height + row_gap first_launch_apply.position = Vector2(0, minf(row_y + 6.0, content_height - 58.0)) first_launch_apply.size = Vector2(settings_width, 58.0) first_launch_error.position = Vector2(0, first_launch_apply.position.y + 60.0) first_launch_error.size = Vector2(settings_width, maxf(24.0, content_height - first_launch_error.position.y)) func _refresh_first_launch_labels() -> void: if first_launch_settings_panel == null: return var values := { "SetupLanguage": _tr("简体中文", "English"), "SetupSafeArea": "%d px" % int(setup_draft.safe_area), "SetupTextScale": "%d%%" % int(round(float(setup_draft.text_scale) * 100.0)), "SetupReducedMotion": _tr("开启", "On") if bool(setup_draft.reduced_motion) else _tr("关闭", "Off"), "SetupReducedFlashes": _tr("开启", "On") if bool(setup_draft.reduced_flashes) else _tr("关闭", "Off"), "SetupCaptions": _tr("开启", "On") if bool(setup_draft.captions) else _tr("关闭", "Off") } for node_name in values: var value_label := first_launch_setting_values.get(node_name) as Label if value_label != null: value_label.text = String(values[node_name]) var heading := first_launch_settings_panel.get_node_or_null("FirstLaunchHeading") as Label if heading != null: heading.text = _tr("首次启动设置", "FIRST-LAUNCH SETUP") var subheading := first_launch_settings_panel.get_node_or_null("FirstLaunchSubheading") as Label if subheading != null: subheading.text = _tr("这些选项只影响显示与辅助功能。", "These choices affect presentation and accessibility only.") if first_launch_apply != null: var label := first_launch_apply.get_node_or_null("ButtonLabel") as Label if label != null: label.text = _tr("应用并进入标题首页", "Apply and continue to Title") func _first_setting_row(row_data: Dictionary) -> Button: var node_name := String(row_data.id) var button := Button.new() button.name = node_name button.flat = true button.focus_mode = Control.FOCUS_ALL button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND for style_name in ["normal", "hover", "pressed", "focus", "disabled", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) var background := _first_nine_patch("Background", FIRST_LAUNCH_SETTING_ROW, 36, 0) background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) button.add_child(background) var icon := TextureRect.new() icon.name = "Icon" icon.texture = FIRST_LAUNCH_SETTING_ICON icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS icon.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(icon) var icon_label := _label(String(row_data.icon), _scaled(15), Color("#f5ead0")) icon_label.name = "IconLabel" icon_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER icon_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER icon_label.mouse_filter = Control.MOUSE_FILTER_IGNORE icon.add_child(icon_label) var label := _label(_tr(String(row_data.label[0]), String(row_data.label[1])), _scaled(13), Color("#202a31")) label.name = "RowLabel" label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS label.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(label) var value_plate := TextureRect.new() value_plate.name = "ValuePlate" value_plate.texture = FIRST_LAUNCH_VALUE_PLATE value_plate.expand_mode = TextureRect.EXPAND_IGNORE_SIZE value_plate.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED value_plate.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS value_plate.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(value_plate) var value_label := _label("", _scaled(12), Color("#202a31")) value_label.name = "ValueLabel" value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER value_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER value_label.mouse_filter = Control.MOUSE_FILTER_IGNORE value_plate.add_child(value_label) first_launch_setting_buttons[node_name] = button first_launch_setting_values[node_name] = value_label button.pressed.connect(row_data.callback, CONNECT_DEFERRED) return button func _layout_first_setting_row(button: Button) -> void: var row_size := button.size var icon := button.get_node("Icon") as TextureRect var label := button.get_node("RowLabel") as Label var value_plate := button.get_node("ValuePlate") as TextureRect var icon_size := clampf(row_size.y - 12.0, 32.0, 54.0) var value_width := clampf(row_size.x * 0.30, 68.0, 132.0) icon.position = Vector2(7.0, (row_size.y - icon_size) * 0.5) icon.size = Vector2(icon_size, icon_size) var value_height := maxf(32.0, row_size.y - 12.0) value_plate.position = Vector2(row_size.x - value_width - 8.0, (row_size.y - value_height) * 0.5) value_plate.size = Vector2(value_width, value_height) label.position = Vector2(icon.position.x + icon_size + 6.0, 0.0) label.size = Vector2(maxf(20.0, value_plate.position.x - label.position.x - 5.0), row_size.y) var value_label := value_plate.get_node("ValueLabel") as Label value_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) func _first_action_button(node_name: String, text_value: String, callback: Callable, texture: Texture2D, height: float) -> Button: var button := Button.new() button.name = node_name button.flat = true button.focus_mode = Control.FOCUS_ALL button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND for style_name in ["normal", "hover", "pressed", "focus", "disabled", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) var frame := _first_nine_patch("CommandFrame", texture, 60, 0) frame.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) button.add_child(frame) var label := _label(text_value, _scaled(15), Color("#213744")) label.name = "ButtonLabel" label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(label) button.custom_minimum_size = Vector2(0, height) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _first_nine_patch(node_name: String, texture: Texture2D, horizontal_margin: int, vertical_margin: int) -> NinePatchRect: var panel := NinePatchRect.new() panel.name = node_name panel.texture = texture panel.patch_margin_left = horizontal_margin panel.patch_margin_right = horizontal_margin panel.patch_margin_top = vertical_margin panel.patch_margin_bottom = vertical_margin panel.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS panel.mouse_filter = Control.MOUSE_FILTER_IGNORE return panel func _first_outline_style(color: Color, width: int, alpha: float) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(0, 0, 0, 0) style.border_color = Color(color.r, color.g, color.b, alpha) style.set_border_width_all(width) return style 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 _refresh_first_launch_labels() func toggle_setup_reduced_motion() -> void: setup_draft.reduced_motion = not bool(setup_draft.reduced_motion) settings = setup_draft.duplicate(true) show_first_launch() func set_setup_reduced_flashes(value: bool) -> void: setup_draft.reduced_flashes = value _refresh_first_launch_labels() func toggle_setup_reduced_flashes() -> void: setup_draft.reduced_flashes = not bool(setup_draft.reduced_flashes) settings = setup_draft.duplicate(true) show_first_launch() func set_setup_captions(value: bool) -> void: setup_draft.captions = value _refresh_first_launch_labels() func toggle_setup_captions() -> void: setup_draft.captions = not bool(setup_draft.captions) settings = setup_draft.duplicate(true) show_first_launch() 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 := first_launch_error 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" title_master_design_canvas = null for child in get_children(): child.free() var world_extension := ColorRect.new() world_extension.name = "TitleMasterWorldExtension" world_extension.color = Color("#8ab8d6") world_extension.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) world_extension.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(world_extension) var safe := Control.new() safe.name = "TitleSafeArea" safe.clip_contents = true safe.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(safe) title_master_design_canvas = safe _layout_title_master_canvas() var master := TextureRect.new() master.name = "TitleMasterComposite" master.texture = TITLE_MASTER_V002 master.expand_mode = TextureRect.EXPAND_IGNORE_SIZE master.stretch_mode = TextureRect.STRETCH_SCALE master.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS master.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) master.mouse_filter = Control.MOUSE_FILTER_IGNORE safe.add_child(master) var layout := Control.new() layout.name = "TitleLayout" layout.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) layout.mouse_filter = Control.MOUSE_FILTER_IGNORE safe.add_child(layout) var header := Control.new() header.name = "TitleHeader" _set_normalized_rect(header, Rect2(0.105, 0.14, 0.275, 0.25)) header.mouse_filter = Control.MOUSE_FILTER_IGNORE layout.add_child(header) var overline := _label(_tr("风路公会 · 远征入口", "WIND-ROAD GUILD"), _scaled(6), Color("#6a4b2d")) overline.name = "TitleOverline" _set_normalized_rect(overline, Rect2(0.10, 0.10, 0.80, 0.16)) overline.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER overline.vertical_alignment = VERTICAL_ALIGNMENT_CENTER overline.mouse_filter = Control.MOUSE_FILTER_IGNORE header.add_child(overline) var locale := String(settings.get("locale", "zh_CN")) var identity := _label(_tr("裂脉远征团", "AETHERBOUND GUILD"), _scaled(14 if locale == "zh_CN" else 11), Color("#213744")) identity.name = "TitleIdentity" _set_normalized_rect(identity, Rect2(0.10, 0.32, 0.80, 0.40)) identity.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER identity.vertical_alignment = VERTICAL_ALIGNMENT_CENTER identity.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS identity.mouse_filter = Control.MOUSE_FILTER_IGNORE header.add_child(identity) var chapter := _label(_tr("第一远征纪", "THE FIRST CHRONICLE"), _scaled(6), Color("#6a4b2d")) chapter.name = "TitleChronicle" _set_normalized_rect(chapter, Rect2(0.18, 0.76, 0.64, 0.16)) chapter.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER chapter.vertical_alignment = VERTICAL_ALIGNMENT_CENTER chapter.mouse_filter = Control.MOUSE_FILTER_IGNORE header.add_child(chapter) var dock := Control.new() dock.name = "TitleCommandDock" _set_normalized_rect(dock, Rect2(0.61, 0.44, 0.32, 0.55)) dock.mouse_filter = Control.MOUSE_FILTER_IGNORE layout.add_child(dock) var copy := Control.new() copy.name = "TitleCopy" copy.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) copy.mouse_filter = Control.MOUSE_FILTER_IGNORE layout.add_child(copy) var actions := Control.new() actions.name = "TitleActions" actions.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) actions.mouse_filter = Control.MOUSE_FILTER_IGNORE copy.add_child(actions) var tools := Control.new() tools.name = "TitleTools" tools.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) tools.mouse_filter = Control.MOUSE_FILTER_IGNORE layout.add_child(tools) 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 new_game_detail := ( _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("创建你的初始远征团", "Create your opening Company") ) var new_game := _title_master_button("TitleNewGame", _tr("开始远征", "New Game"), new_game_detail, open_new_game_boundary, Rect2(0.62, 0.515, 0.23, 0.19), "primary") 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_master_button_disabled(new_game, new_game.disabled) actions.add_child(new_game) var continue_reason := ( _tr("本地数据需要处理", "Local data needs attention") if data_unsafe else _tr("暂无可恢复记录", "No resumable expedition") if save_status != "valid" else _tr("进入存档确认", "Review the saved expedition") ) var continue_button := _title_master_button("TitleContinue", _tr("继续游戏", "Continue"), continue_reason, open_continue_boundary, Rect2(0.635, 0.68, 0.18, 0.13), "secondary") var continue_detail := continue_button.get_node("DetailLabel") as Label continue_detail.name = "TitleContinueReason" 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_master_button_disabled(continue_button, continue_button.disabled) actions.add_child(continue_button) var notice := _label(title_notice, _scaled(7), Color("#914b32")) notice.name = "TitleNotice" _set_normalized_rect(notice, Rect2(0.08, 0.57, 0.84, 0.34)) notice.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER notice.vertical_alignment = VERTICAL_ALIGNMENT_CENTER notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART notice.mouse_filter = Control.MOUSE_FILTER_IGNORE continue_detail.visible = title_notice.is_empty() continue_button.add_child(notice) var summary := new_game.get_node("DetailLabel") as Label summary.name = "TitleSaveSummary" var settings_button := _title_master_button("TitleSettings", "", "", show_settings_placeholder, Rect2(0.61, 0.755, 0.075, 0.13), "utility") settings_button.tooltip_text = _tr("设置", "Settings") tools.add_child(settings_button) var credits_button := _title_master_button("TitleCredits", _tr("制作人员", "Credits"), "", show_credits_placeholder, Rect2(0.645, 0.78, 0.13, 0.10), "utility") tools.add_child(credits_button) (new_game if not new_game.disabled else continue_button if not continue_button.disabled else settings_button).call_deferred("grab_focus") func _layout_title_master_canvas() -> void: if not is_instance_valid(title_master_design_canvas): return var available := size if available.x <= 0.0 or available.y <= 0.0: return const DESIGN_ASPECT := 16.0 / 9.0 var canvas_size: Vector2 if available.x / available.y >= DESIGN_ASPECT: canvas_size = Vector2(available.y * DESIGN_ASPECT, available.y) else: canvas_size = Vector2(available.x, available.x / DESIGN_ASPECT) title_master_design_canvas.position = (available - canvas_size) * 0.5 title_master_design_canvas.size = canvas_size 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_opening(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 show_guild_anchor(resolved) return current_screen = "guild_handoff" _build_world_base(true) 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", "content_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.visible = not active_contract and not 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.visible = 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")) var focus_target := contract if contract.visible else market if market.visible else footer.get_node("GuildRosterButton") focus_target.call_deferred("grab_focus") func show_guild_anchor(company: Dictionary) -> void: _show_guild_master_composite(company) return func _show_guild_master_composite(company: Dictionary) -> void: _show_guild_master_composite_exact(company) return func _show_guild_master_composite_exact(company: Dictionary) -> void: var resolved: Dictionary = company.duplicate(true) guild_company = resolved current_screen = "guild_handoff" # Guild can be opened directly by a resumed save or a capture fixture, so use # the live Settings locale/scale instead of a stale first-launch draft. setup_draft.locale = String(settings.get("locale", setup_draft.get("locale", "zh_CN"))) setup_draft.text_scale = float(settings.get("text_scale", setup_draft.get("text_scale", 1.0))) guild_master_design_canvas = null for child in get_children(): child.free() var world_extension := TextureRect.new() world_extension.name = "GuildMasterWorldExtension" world_extension.texture = GUILD_DESK_WORLD world_extension.expand_mode = TextureRect.EXPAND_IGNORE_SIZE world_extension.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED world_extension.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS world_extension.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) world_extension.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(world_extension) var design_canvas := Control.new() design_canvas.name = "GuildMasterDesignCanvas" design_canvas.clip_contents = true design_canvas.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(design_canvas) guild_master_design_canvas = design_canvas _layout_guild_master_canvas() var background := TextureRect.new() background.name = "GuildMasterLayeredWorld" background.texture = GUILD_DESK_WORLD background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE background.stretch_mode = TextureRect.STRETCH_SCALE background.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) background.mouse_filter = Control.MOUSE_FILTER_IGNORE design_canvas.add_child(background) var root := Control.new() root.name = "GuildAnchorRoot" root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) root.mouse_filter = Control.MOUSE_FILTER_STOP design_canvas.add_child(root) _guild_desk_segmented_rail( root, "GuildTopRail", Rect2(0.012, 0.010, 0.976, 0.115), GUILD_DESK_TOP_LEFT, GUILD_DESK_TOP_CENTER, GUILD_DESK_TOP_RIGHT, 0.055, 0.046 ) _guild_desk_segmented_rail( root, "GuildBottomRail", Rect2(0.010, 0.865, 0.980, 0.130), GUILD_DESK_BOTTOM_LEFT, GUILD_DESK_BOTTOM_CENTER, GUILD_DESK_BOTTOM_RIGHT, 0.080, 0.090 ) var active_contract: bool = not resolved.active_contract.is_empty() var title_plate := _guild_master_overlay_plate(root, "GuildTitlePlate", Rect2(0.090, 0.026, 0.137, 0.076), Color("#f3dfae"), Color("#9d7839"), 2, 14) var title := _label(_tr("公会大厅", "GUILD HALL"), _guild_text_size(11), Color("#23323a")) title.name = "GuildAnchorTitle" _set_normalized_rect(title, Rect2(0.07, 0.04, 0.86, 0.54)) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER title.mouse_filter = Control.MOUSE_FILTER_IGNORE title_plate.add_child(title) var status := _label(_tr("持续远征团", "PERSISTENT COMPANY"), _guild_text_size(6), Color("#5e513a")) status.name = "GuildAnchorStatus" _set_normalized_rect(status, Rect2(0.07, 0.57, 0.86, 0.34)) status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER status.mouse_filter = Control.MOUSE_FILTER_IGNORE title_plate.add_child(status) var pressure_copy := "0" var coin_copy := "--" if active_contract: pressure_copy = _difficulty_name(String(resolved.active_contract.get("difficulty", "standard"))) coin_copy = "%d" % int(resolved.active_contract.get("coin", 0)) var pressure_status := _guild_desk_status_slot(root, "GuildProgressStatus", Rect2(0.595, 0.020, 0.110, 0.090), pressure_copy, "pressure") pressure_status.tooltip_text = _tr("当前委托压力", "Current Contract pressure") var coin_status := _guild_desk_status_slot(root, "GuildCoinStatus", Rect2(0.735, 0.020, 0.100, 0.090), coin_copy, "coin") coin_status.tooltip_text = _tr("当前委托金币", "Current Contract Coin") var company_status := _guild_desk_status_slot(root, "GuildAnchorCompanyStatus", Rect2(0.845, 0.020, 0.090, 0.090), "%d" % resolved.owned_recruits.size(), "company") company_status.tooltip_text = _tr("远征团成员数", "Expedition Company members") var settings_button := _guild_desk_icon_button(root, "GuildSettings", _tr("设置", "Settings"), "settings", show_settings_from_guild, Rect2(0.922, 0.004, 0.076, 0.136)) settings_button.tooltip_text = _tr("设置", "Settings") var title_button := _guild_master_hit_button("GuildTitle", _tr("返回标题", "Back to Title"), back_to_title, Rect2(0.000, 0.000, 0.075, 0.140)) title_button.set_meta("art_owned_control", "GuildTopRailLeft") var title_command_label := _label(_tr("标题", "Title"), _guild_text_size(8), Color.TRANSPARENT) title_command_label.name = "GuildTitleCommandLabel" title_command_label.visible = false title_command_label.mouse_filter = Control.MOUSE_FILTER_IGNORE title_button.add_child(title_command_label) root.add_child(title_button) var roster := Control.new() roster.name = "GuildRoster" roster.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) roster.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(roster) for index in range(mini(2, resolved.owned_recruits.size())): var recruit: Dictionary = resolved.owned_recruits[index] var member_rect := Rect2(0.190 + index * 0.190, 0.250 - index * 0.010, 0.280, 0.600) roster.add_child(_guild_desk_member(index, recruit, member_rect)) var identity_copy := "" if String(settings.get("locale", "zh_CN")) == "zh_CN": identity_copy = "%s · %s" % [_recruit_name(recruit), _recruit_profession_name(recruit)] else: identity_copy = "%s · %s" % [_recruit_name(recruit), _recruit_profession_name(recruit)] var identity_plate := _guild_master_overlay_plate(root, "GuildMasterIdentityPlate%d" % index, Rect2(0.225 + index * 0.190, 0.805, 0.180, 0.050), Color(0.025, 0.09, 0.14, 0.86), Color(0.68, 0.51, 0.26, 0.64), 1, 2) var identity := _label(identity_copy, _guild_text_size(8), Color("#fff4d6")) identity.name = "GuildMasterIdentity%d" % index identity.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) identity.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER identity.vertical_alignment = VERTICAL_ALIGNMENT_CENTER identity.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS identity.add_theme_color_override("font_outline_color", Color(0.02, 0.04, 0.05, 0.92)) identity.add_theme_constant_override("outline_size", maxi(1, _guild_text_size(1))) identity.mouse_filter = Control.MOUSE_FILTER_IGNORE identity_plate.add_child(identity) var kiosk := TextureRect.new() kiosk.name = "GuildMasterKioskTexture" kiosk.texture = GUILD_DESK_CONTRACT_BOARD kiosk.expand_mode = TextureRect.EXPAND_IGNORE_SIZE kiosk.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED kiosk.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS _set_normalized_rect(kiosk, Rect2(0.615, 0.205, 0.370, 0.655)) kiosk.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(kiosk) var kiosk_copy_area := Control.new() kiosk_copy_area.name = "GuildKioskCopyArea" _set_normalized_rect(kiosk_copy_area, Rect2(0.714, 0.433, 0.155, 0.305)) kiosk_copy_area.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(kiosk_copy_area) var kiosk_text := _label(_tr("远征市场", "EXPEDITION MARKET") if active_contract else _tr("委托板", "CONTRACT BOARD"), _guild_text_size(11), Color("#2b3030")) kiosk_text.name = "GuildKioskText" _set_normalized_rect(kiosk_text, Rect2(0.03, 0.04, 0.94, 0.20)) kiosk_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER kiosk_text.vertical_alignment = VERTICAL_ALIGNMENT_CENTER kiosk_text.mouse_filter = Control.MOUSE_FILTER_IGNORE kiosk_copy_area.add_child(kiosk_text) var contract_reason := _label(_tr("委托进行中", "CONTRACT ACTIVE") if active_contract else _tr("可用 · 选择难度", "AVAILABLE · CHOOSE DIFFICULTY"), _guild_text_size(8), Color("#4b3724")) contract_reason.name = "GuildContractReason" _set_normalized_rect(contract_reason, Rect2(0.04, 0.31, 0.92, 0.24)) contract_reason.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER contract_reason.vertical_alignment = VERTICAL_ALIGNMENT_CENTER contract_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART contract_reason.mouse_filter = Control.MOUSE_FILTER_IGNORE kiosk_copy_area.add_child(contract_reason) var market_reason := _label(_tr("市场已开放", "Market open") if active_contract else _tr("市场锁定 · 先确认首个委托", "Market locked · Confirm first Contract"), _guild_text_size(8), Color("#6a4a2d")) market_reason.name = "GuildMarketReason" _set_normalized_rect(market_reason, Rect2(0.04, 0.58, 0.92, 0.28)) market_reason.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER market_reason.vertical_alignment = VERTICAL_ALIGNMENT_CENTER market_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART market_reason.mouse_filter = Control.MOUSE_FILTER_IGNORE kiosk_copy_area.add_child(market_reason) var notice := _label("", _guild_text_size(8), Color("#f0ca75")) notice.name = "GuildNotice" _set_normalized_rect(notice, Rect2(0.03, 0.86, 0.94, 0.13)) notice.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER notice.vertical_alignment = VERTICAL_ALIGNMENT_CENTER notice.mouse_filter = Control.MOUSE_FILTER_IGNORE kiosk_copy_area.add_child(notice) var roster_button := _guild_desk_nav_button(root, "GuildRosterButton", "GuildRosterCommandLabel", _tr("成员", "Roster"), "roster", show_roster.bind(resolved), 0, false) var equipment_button := _guild_desk_nav_button(root, "GuildEquipment", "GuildEquipmentCommandLabel", _tr("装备", "Gear"), "equipment", func(): _set_guild_notice(_tr("建立委托后可在市场查看装备。", "Create a Contract to review equipment in its Market.")), 1, not active_contract) var party_button := _guild_desk_nav_button(root, "GuildParty", "GuildPartyCommandLabel", _tr("队列", "Party"), "party", open_party_line if active_contract else func(): _set_guild_notice(_tr("先建立委托。", "Create a Contract first.")), 2, not active_contract) var risk_button := _guild_desk_nav_button(root, "GuildRisk", "GuildRouteStatusLabel", _tr("风险", "Risk"), "risk", open_risk_board if active_contract else func(): _set_guild_notice(_tr("先建立委托。", "Create a Contract first.")), 3, not active_contract) for nav_button in [title_button, roster_button, equipment_button, party_button, risk_button]: nav_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND var contract := _guild_master_hit_button("GuildContractActive" if active_contract else "GuildContractBoard", _tr("查看委托板", "Open Contract Board"), show_contract_choice, Rect2(0.615, 0.205, 0.370, 0.655)) contract.disabled = active_contract if active_contract: contract.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(contract) var market := _guild_master_hit_button("GuildMarket" if active_contract else "GuildMarketUnavailable", _tr("进入市场", "Enter Market"), show_market_handoff.bind(resolved) if active_contract else func(): _set_guild_notice(_tr("请先确认首个委托。", "Confirm the first Contract first.")), Rect2(0.850, 0.840, 0.140, 0.155)) market.disabled = not active_contract root.add_child(market) for marker_name in ["GuildContractSticker", "GuildMarketSticker"]: var marker := Control.new() marker.name = marker_name marker.visible = false root.add_child(marker) contract.call_deferred("grab_focus") func _layout_guild_master_canvas() -> void: if not is_instance_valid(guild_master_design_canvas): return var available := size if available.x <= 0.0 or available.y <= 0.0: return const DESIGN_ASPECT := 16.0 / 9.0 var canvas_size: Vector2 if available.x / available.y >= DESIGN_ASPECT: canvas_size = Vector2(available.y * DESIGN_ASPECT, available.y) else: canvas_size = Vector2(available.x, available.x / DESIGN_ASPECT) guild_master_design_canvas.position = (available - canvas_size) * 0.5 guild_master_design_canvas.size = canvas_size _refresh_guild_typography() _refresh_guild_bottom_nav_geometry() func _refresh_guild_typography() -> void: var label_sizes := { "GuildAnchorTitle": 11, "GuildAnchorStatus": 6, "GuildProgressStatus": 9, "GuildCoinStatus": 9, "GuildAnchorCompanyStatus": 9, "GuildKioskText": 11, "GuildContractReason": 8, "GuildMarketReason": 8, "GuildNotice": 8, "GuildMasterIdentity0": 8, "GuildMasterIdentity1": 8, "GuildTitleCommandLabel": 8, "GuildRosterCommandLabel": 8, "GuildEquipmentCommandLabel": 8, "GuildPartyCommandLabel": 8, "GuildRouteStatusLabel": 8, } for node_name in label_sizes: var label := find_child(String(node_name), true, false) as Label if label != null: label.add_theme_font_size_override("font_size", _guild_text_size(int(label_sizes[node_name]))) if label.has_theme_constant_override("outline_size"): label.add_theme_constant_override("outline_size", maxi(1, _guild_text_size(1))) func _refresh_guild_bottom_nav_geometry() -> void: for index in range(4): var button_names := ["GuildRosterButton", "GuildEquipment", "GuildParty", "GuildRisk"] var button := find_child(button_names[index], true, false) as Button if button != null: _set_guild_bottom_nav_rect(button, index) func _set_guild_bottom_nav_rect(control: Control, index: int) -> void: var canvas_height := guild_master_design_canvas.size.y if is_instance_valid(guild_master_design_canvas) else size.y var target_size := clampf(canvas_height * 0.145, 50.0, 76.0) var gap := clampf(canvas_height * 0.012, 4.0, 8.0) var start_x := clampf(canvas_height * 0.035, 10.0, 25.0) var center_y := minf(canvas_height * 0.945, canvas_height - target_size * 0.5 - 2.0) control.set_anchors_preset(Control.PRESET_TOP_LEFT) control.position = Vector2(start_x + float(index) * (target_size + gap), center_y - target_size * 0.5) control.size = Vector2(target_size, target_size) func _guild_desk_segmented_rail(parent: Control, node_name: String, rect: Rect2, left_texture: Texture2D, center_texture: Texture2D, right_texture: Texture2D, left_fraction: float, right_fraction: float) -> Control: var rail := Control.new() rail.name = node_name _set_normalized_rect(rail, rect) rail.mouse_filter = Control.MOUSE_FILTER_IGNORE parent.add_child(rail) var left := TextureRect.new() left.name = "%sLeft" % node_name left.texture = left_texture left.expand_mode = TextureRect.EXPAND_IGNORE_SIZE left.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED left.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS left.anchor_left = 0.0 left.anchor_top = 0.0 left.anchor_right = left_fraction left.anchor_bottom = 1.0 left.offset_left = 0 left.offset_top = 0 left.offset_right = 0 left.offset_bottom = 0 left.mouse_filter = Control.MOUSE_FILTER_IGNORE rail.add_child(left) var center := TextureRect.new() center.name = "%sCenter" % node_name center.texture = center_texture center.expand_mode = TextureRect.EXPAND_IGNORE_SIZE center.stretch_mode = TextureRect.STRETCH_SCALE center.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS center.anchor_left = left_fraction center.anchor_right = 1.0 - right_fraction center.anchor_bottom = 1.0 center.offset_left = 0 center.offset_top = 0 center.offset_right = 0 center.offset_bottom = 0 center.mouse_filter = Control.MOUSE_FILTER_IGNORE rail.add_child(center) var right := TextureRect.new() right.name = "%sRight" % node_name right.texture = right_texture right.expand_mode = TextureRect.EXPAND_IGNORE_SIZE right.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED right.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS right.anchor_left = 1.0 - right_fraction right.anchor_right = 1.0 right.anchor_bottom = 1.0 right.offset_left = 0 right.offset_top = 0 right.offset_right = 0 right.offset_bottom = 0 right.mouse_filter = Control.MOUSE_FILTER_IGNORE rail.add_child(right) return rail func _guild_desk_status_slot(parent: Control, node_name: String, rect: Rect2, value: String, icon_kind: String) -> Label: var slot := Control.new() slot.name = "%sSlot" % node_name _set_normalized_rect(slot, rect) slot.mouse_filter = Control.MOUSE_FILTER_IGNORE parent.add_child(slot) var pill := Panel.new() pill.name = "%sPill" % node_name pill.anchor_left = 0.25 pill.anchor_top = 0.24 pill.anchor_right = 0.98 pill.anchor_bottom = 0.76 pill.add_theme_stylebox_override("panel", _flat_style(Color("#f2dfb4"), Color("#9d7839"), 1, 12)) pill.mouse_filter = Control.MOUSE_FILTER_IGNORE slot.add_child(pill) var badge := Control.new() badge.name = "%sIcon" % node_name badge.anchor_left = 0.00 badge.anchor_top = 0.02 badge.anchor_right = 0.36 badge.anchor_bottom = 0.98 badge.mouse_filter = Control.MOUSE_FILTER_IGNORE badge.add_child(_entry_chrome("Chrome", "guild_status_icon", icon_kind)) slot.add_child(badge) var label := _label(value, _guild_text_size(8), Color("#2f3130")) label.name = node_name label.anchor_left = 0.36 label.anchor_top = 0.24 label.anchor_right = 0.96 label.anchor_bottom = 0.76 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 slot.add_child(label) return label func _guild_desk_icon_button(parent: Control, node_name: String, tooltip: String, icon_kind: String, callback: Callable, rect: Rect2) -> Button: var button := _guild_master_hit_button(node_name, tooltip, callback, rect) var chrome := _entry_chrome("Chrome", "guild_status_icon", icon_kind) chrome.anchor_left = 0.20 button.add_child(chrome) button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND parent.add_child(button) return button func _guild_desk_nav_button(parent: Control, node_name: String, label_name: String, title: String, icon_kind: String, callback: Callable, index: int, disabled: bool) -> Button: var button := _guild_master_hit_button(node_name, title, callback, Rect2()) _set_guild_bottom_nav_rect(button, index) button.disabled = disabled button.add_child(_entry_chrome("Chrome", "guild_nav_icon", icon_kind)) var label := _label(title, _guild_text_size(8), Color("#f8e7b5") if not disabled else Color("#8f9698")) label.name = label_name label.visible = false label.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(label) parent.add_child(button) return button func _guild_desk_slot_art(parent: Control, node_name: String, texture: Texture2D, rect: Rect2) -> TextureRect: var art := TextureRect.new() art.name = node_name art.texture = texture art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED art.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS _set_normalized_rect(art, rect) art.mouse_filter = Control.MOUSE_FILTER_IGNORE parent.add_child(art) return art func _show_guild_master_composite_legacy(company: Dictionary) -> void: var resolved := company.duplicate(true) guild_company = resolved current_screen = "guild_handoff" _build_world_base(true) var guild_backdrop := find_child("WorldBackground", true, false) as TextureRect if guild_backdrop != null: guild_backdrop.texture = GUILD_MASTER_BACKGROUND guild_backdrop.stretch_mode = TextureRect.STRETCH_SCALE var root := Control.new() root.name = "GuildAnchorRoot" root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) root.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(root) var top_rail := TextureRect.new() top_rail.name = "GuildAnchorTopRail" top_rail.texture = GUILD_MASTER_TOP_RAIL top_rail.expand_mode = TextureRect.EXPAND_IGNORE_SIZE top_rail.stretch_mode = TextureRect.STRETCH_SCALE top_rail.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS top_rail.mouse_filter = Control.MOUSE_FILTER_IGNORE _set_normalized_rect(top_rail, Rect2(0.008, 0.012, 0.976, 0.125)) root.add_child(top_rail) var title := _label(_tr("公会大厅", "GUILD HALL"), _scaled(17), Color("#fff4d6")) title.name = "GuildAnchorTitle" _set_normalized_rect(title, Rect2(0.13, 0.025, 0.20, 0.06)) root.add_child(title) var status := _label(_tr("持续远征", "PERSISTENT COMPANY"), _scaled(9), Color("#a9d9ef")) status.name = "GuildAnchorStatus" _set_normalized_rect(status, Rect2(0.132, 0.080, 0.19, 0.035)) root.add_child(status) var company_status := _label(_tr("成员 %d", "%d MEMBERS") % resolved.owned_recruits.size(), _scaled(13), Color("#f0ca75")) company_status.name = "GuildAnchorCompanyStatus" _set_normalized_rect(company_status, Rect2(0.78, 0.050, 0.13, 0.06)) company_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT root.add_child(company_status) var body := Control.new() body.name = "GuildHandoffBody" body.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) body.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(body) var roster := Control.new() roster.name = "GuildRoster" roster.anchor_right = 1.0 roster.anchor_bottom = 1.0 roster.mouse_filter = Control.MOUSE_FILTER_IGNORE body.add_child(roster) for index in range(resolved.owned_recruits.size()): var anchor_texture: Texture2D = GUILD_MASTER_BELLKEEPER if index == 0 else GUILD_MASTER_SCOUT if index == 1 else null var actor_rect := Rect2(0.235, 0.385, 0.19, 0.53) if index == 0 else Rect2(0.405, 0.405, 0.18, 0.51) if index == 1 else Rect2(0.20 + index * 0.13, 0.43, 0.14, 0.42) roster.add_child(_guild_master_member(index, resolved.owned_recruits[index], anchor_texture, actor_rect)) var next := Control.new() next.name = "GuildNextActions" next.anchor_right = 1.0 next.anchor_bottom = 1.0 next.mouse_filter = Control.MOUSE_FILTER_IGNORE body.add_child(next) var active_contract: bool = String(resolved.get("active_contract", {}).get("status", "")) in ["market", "battle", "result", "recovery"] var returned_contract: bool = String(resolved.get("active_contract", {}).get("status", "")) == "returned" var next_label := _label(_tr("下一步", "NEXT ACTION"), _scaled(10), Color("#e8d39d")) next_label.name = "GuildNextHeading" _set_normalized_rect(next_label, Rect2(0.625, 0.275, 0.27, 0.045)) next_label.visible = false next.add_child(next_label) var next_copy := _label( _tr("前往市场扩充远征团。", "Enter the Market to expand the Company.") if active_contract else _tr("选择首个委托与难度。", "Choose the first Contract and difficulty."), _scaled(11), Color("#fff4d6") ) next_copy.name = "GuildNextCopy" _set_normalized_rect(next_copy, Rect2(0.625, 0.315, 0.28, 0.07)) next_copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next_copy.visible = false next.add_child(next_copy) var contract := _guild_master_kiosk_button( "GuildContractActive" if active_contract else "GuildContractBoard", "contract", _tr("委托已开始", "Contract Active") if active_contract else _tr("委托板", "Contract Board"), func(): _set_guild_notice(_tr("委托已开始,请进入市场。", "The Contract is active; enter its Market.")) if active_contract else show_contract_choice(), Rect2(0.625, 0.300, 0.31, 0.53) ) _set_entry_button_disabled(contract, active_contract or returned_contract) contract.visible = not active_contract and not returned_contract next.add_child(contract) var contract_reason := _label( _tr("已开始:风镜初渡", "Active: First Windglass Crossing") if active_contract else _tr("可用:选择难度后确认", "Available: choose a difficulty, then confirm"), _scaled(10), Color("#e8d39d") ) contract_reason.name = "GuildContractReason" _set_normalized_rect(contract_reason, Rect2(0.645, 0.765, 0.28, 0.05)) contract_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next.add_child(contract_reason) var market := _guild_master_kiosk_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("请先确认首个委托。", "Confirm the first Contract first.")), Rect2(0.625, 0.300, 0.31, 0.53) ) _set_entry_button_disabled(market, not active_contract) market.visible = active_contract next.add_child(market) var market_reason := _label( _tr("可用:市场准备", "Available: Market preparation") if active_contract else _tr("不可用:尚未建立首个委托。", "Unavailable: no first Contract has been created."), _scaled(10), Color("#e8d39d") ) market_reason.name = "GuildMarketReason" _set_normalized_rect(market_reason, Rect2(0.645, 0.815, 0.28, 0.05)) market_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next.add_child(market_reason) var notice := _label("", _scaled(10), Color("#f0ca75")) notice.name = "GuildNotice" _set_normalized_rect(notice, Rect2(0.645, 0.865, 0.28, 0.04)) notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART next.add_child(notice) var bottom_rail := TextureRect.new() bottom_rail.name = "GuildAnchorBottomRail" bottom_rail.texture = GUILD_MASTER_BOTTOM_RAIL bottom_rail.expand_mode = TextureRect.EXPAND_IGNORE_SIZE bottom_rail.stretch_mode = TextureRect.STRETCH_SCALE bottom_rail.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS bottom_rail.mouse_filter = Control.MOUSE_FILTER_IGNORE _set_normalized_rect(bottom_rail, Rect2(0.008, 0.845, 0.976, 0.15)) root.add_child(bottom_rail) var footer := Control.new() footer.name = "GuildHandoffFooter" footer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) footer.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(footer) var back := _guild_master_hit_button("GuildTitle", _tr("返回标题", "Back to Title"), back_to_title, Rect2(0.035, 0.875, 0.075, 0.105)) footer.add_child(back) var roster_button := _guild_master_hit_button("GuildRosterButton", _tr("成员", "Roster"), show_roster.bind(resolved), Rect2(0.105, 0.875, 0.075, 0.105)) footer.add_child(roster_button) var settings_button := _guild_master_hit_button("GuildSettings", _tr("设置", "Settings"), show_settings_from_guild, Rect2(0.175, 0.875, 0.075, 0.105)) footer.add_child(settings_button) (contract if contract.visible else market if market.visible else roster_button).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, 40 if size.y < 380.0 else (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(46 if size.y < 380.0 else 58, 40 if size.y < 380.0 else (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, 48 if size.y < 380.0 else (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, 3) portrait.size = Vector2(42 if size.y < 380.0 else 52, 42 if size.y < 380.0 else 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(58 if size.y < 380.0 else 70, 3 if size.y < 380.0 else 6) name.size = Vector2(230, 20 if size.y < 380.0 else 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(58 if size.y < 380.0 else 70, 24 if size.y < 380.0 else 31) facts.size = Vector2(290, 20 if size.y < 380.0 else 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(true) var frame_safe := _framed_page_safe("ContractChoice") frame_safe.add_child(_entry_chrome("ContractChoiceFrame", "content_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, "content_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(true) var frame_safe := _framed_page_safe("Market") frame_safe.add_child(_entry_chrome("MarketFrame", "content_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_party_master_base() _build_party_master_ui(resolved, formation) 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" var front_recruit := _recruit_by_id(resolved, String(party_draft_order[0])) var coverage: Dictionary = formation.coverage _build_readiness_master_base() _build_readiness_master_ui(resolved, formation, front_recruit, coverage) func _build_readiness_master_base() -> void: for child in get_children(): child.free() var matte := ColorRect.new() matte.name = "ReadinessMasterMatte" matte.color = Color("#17252c") matte.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) matte.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(matte) var canvas := Control.new() canvas.name = "ReadinessMasterCanvas" canvas.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(canvas) readiness_master_design_canvas = canvas _layout_readiness_master_canvas() var master := TextureRect.new() master.name = "ReadinessStaticMaster" var source := Image.load_from_file(ProjectSettings.globalize_path(READINESS_STATIC_MASTER_PATH)) master.texture = ImageTexture.create_from_image(source) master.expand_mode = TextureRect.EXPAND_IGNORE_SIZE master.stretch_mode = TextureRect.STRETCH_SCALE master.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) master.mouse_filter = Control.MOUSE_FILTER_IGNORE canvas.add_child(master) func _layout_readiness_master_canvas() -> void: if not is_instance_valid(readiness_master_design_canvas): return var available := size if available.x <= 0.0 or available.y <= 0.0: return const DESIGN_ASPECT := 16.0 / 9.0 var canvas_size: Vector2 if available.x / available.y >= DESIGN_ASPECT: canvas_size = Vector2(available.y * DESIGN_ASPECT, available.y) else: canvas_size = Vector2(available.x, available.x / DESIGN_ASPECT) readiness_master_design_canvas.position = (available - canvas_size) * 0.5 readiness_master_design_canvas.size = canvas_size func _build_readiness_master_ui(resolved: Dictionary, formation: Dictionary, front_recruit: Dictionary, coverage: Dictionary) -> void: var canvas := readiness_master_design_canvas if not is_instance_valid(canvas): return var overlay := Control.new() overlay.name = "ReadinessMasterOverlay" overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) overlay.mouse_filter = Control.MOUSE_FILTER_PASS canvas.add_child(overlay) _readiness_master_label(overlay, "ReadinessTitle", Rect2(0.19, 0.035, 0.10, 0.055), _tr("准备", "READY"), 9, Color("#242c2b")) _readiness_master_label(overlay, "ReadinessSnapshot", Rect2(0.285, 0.035, 0.075, 0.055), _tr("冻结", "FROZEN"), 8, Color("#514331")) _readiness_master_label(overlay, "ReadinessContract", Rect2(0.40, 0.035, 0.14, 0.055), String(resolved.active_contract.get("contract_id", "CTR-R1-01")), 8, Color("#514331")) _readiness_master_label(overlay, "ReadinessPressure", Rect2(0.61, 0.035, 0.13, 0.055), _party_pressure_short(String(resolved.active_contract.get("difficulty", "standard"))), 8, Color("#514331")) _readiness_master_label(overlay, "ReadinessCompany", Rect2(0.82, 0.035, 0.12, 0.055), "%d/4" % resolved.owned_recruits.size(), 8, Color("#514331")) var settings_button := _readiness_master_action_button("ReadinessSettings", Rect2(0.88, 0.0, 0.10, 0.14), _readiness_open_settings, _tr("打开设置", "Open settings")) overlay.add_child(settings_button) var cord_track := PartyLineTrack.new() cord_track.name = "ReadinessFormationCord" cord_track.render_layer = PartyLineTrack.RenderLayer.CORD cord_track.z_index = 25 cord_track.set_meta("render_layer", "cord") _party_master_place(cord_track, Rect2(0.08, 0.30, 0.80, 0.37)) cord_track.mouse_filter = Control.MOUSE_FILTER_IGNORE overlay.add_child(cord_track) var member_areas := [ Rect2(0.61, 0.29, 0.15, 0.30), Rect2(0.46, 0.30, 0.14, 0.29), Rect2(0.31, 0.30, 0.14, 0.29), Rect2(0.16, 0.30, 0.14, 0.29), ] var member_nodes: Array[Control] = [] for visual_index in range(party_draft_order.size() - 1, -1, -1): var recruit_id := String(party_draft_order[visual_index]) var recruit := _recruit_by_id(resolved, recruit_id) var button := _readiness_master_member_button(visual_index, recruit, member_areas[visual_index]) button.z_index = 30 overlay.add_child(button) member_nodes.append(button) var identity := _readiness_master_label(overlay, "ReadinessMember%dIdentity" % visual_index, Rect2(0.19 + float(3 - visual_index) * 0.15, 0.595, 0.12, 0.045), _recruit_name(recruit), 8, Color("#2b241b")) identity.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS var effects_track := PartyLineTrack.new() effects_track.name = "ReadinessFormationEffects" effects_track.render_layer = PartyLineTrack.RenderLayer.EFFECTS effects_track.z_index = 35 effects_track.set_meta("render_layer", "effects") _party_master_place(effects_track, Rect2(0.08, 0.30, 0.80, 0.37)) effects_track.mouse_filter = Control.MOUSE_FILTER_IGNORE effects_track.bind(member_nodes, "", String(settings.get("locale", "zh_CN"))) overlay.add_child(effects_track) cord_track.bind(member_nodes, "", String(settings.get("locale", "zh_CN"))) var summary_band := Panel.new() summary_band.name = "ReadinessSummaryBand" _readiness_master_place(summary_band, Rect2(0.04, 0.645, 0.73, 0.19)) var summary_style := StyleBoxFlat.new() summary_style.bg_color = Color("#f2e4c5", 0.90) summary_style.border_color = Color("#a27e3c", 0.86) summary_style.set_border_width_all(2) summary_style.set_corner_radius_all(7) summary_band.add_theme_stylebox_override("panel", summary_style) summary_band.mouse_filter = Control.MOUSE_FILTER_IGNORE summary_band.z_index = 20 overlay.add_child(summary_band) var summary := _readiness_master_label( overlay, "ReadinessFrozenFacts", Rect2(0.06, 0.66, 0.69, 0.055), _tr("0 号首要目标:%s · 四人同时出战 · 无自动替补", "Position 0 primary exposure: %s · all four deploy · no automatic Reserve") % _recruit_name(front_recruit), 9, Color("#2b241b") ) summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER summary.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _readiness_master_label(overlay, "ReadinessProtection", Rect2(0.09, 0.735, 0.14, 0.055), _tr("保护 %d", "GUARD %d") % int(coverage.get("protection", 0)), 8, Color("#2b241b")) _readiness_master_label(overlay, "ReadinessReach", Rect2(0.28, 0.735, 0.14, 0.055), _tr("射程 %d", "REACH %d") % int(coverage.get("long_reach", 0)), 8, Color("#2b241b")) _readiness_master_label(overlay, "ReadinessPressureCoverage", Rect2(0.47, 0.735, 0.16, 0.055), _tr("压力 %d", "PRESSURE %d") % int(coverage.get("control", 0)), 8, Color("#2b241b")) _readiness_master_label(overlay, "ReadinessReserve", Rect2(0.67, 0.735, 0.13, 0.055), _tr("预备 0", "RESERVE 0"), 8, Color("#2b241b")) var warning_button := _readiness_warning_button(formation) _readiness_master_place(warning_button, Rect2(0.78, 0.56, 0.21, 0.23)) warning_button.z_index = 45 overlay.add_child(warning_button) var knockout := _readiness_master_label(overlay, "ReadinessKnockoutRule", Rect2(0.78, 0.42, 0.21, 0.07), _tr("击倒后下关归队", "KNOCKOUT RETURNS NEXT STAGE"), 7, Color("#f5ead0")) knockout.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART if String(formation.get("status", "draft")) == "ready": var receipt := _readiness_master_label(overlay, "ReadinessReceipt", Rect2(0.78, 0.49, 0.21, 0.06), _tr("回执已保存", "RECEIPT SAVED"), 7, Color("#9ed7aa")) receipt.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS var risk_hint := _readiness_master_label( overlay, "ReadinessRiskUnavailable", Rect2(0.78, 0.27, 0.21, 0.22), _tr("接受后\n比较风险", "ACCEPT\nTHEN RISK"), 8, Color("#f0ca75") if String(formation.get("status", "draft")) != "ready" else Color("#9ed7aa") ) risk_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART risk_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER risk_hint.vertical_alignment = VERTICAL_ALIGNMENT_CENTER var revise := _readiness_master_command_button("ReadinessRevise", READINESS_COMMAND_REVISE, _tr("修改队列", "REVISE"), revise_readiness, Rect2(0.14, 0.85, 0.17, 0.14), false, _tr("返回队列并重新保存", "Return to the Party Line and save again"), false) var back := _readiness_master_command_button("ReadinessBack", READINESS_COMMAND_BACK, _tr("返回公会", "BACK"), cancel_party_line, Rect2(0.29, 0.85, 0.15, 0.14), false, _tr("返回公会,不改变已保存顺序", "Return to Guild without changing the saved order"), false) var accept := _readiness_master_command_button("ReadinessAccept", READINESS_COMMAND_ACCEPT, _tr("接受准备", "ACCEPT"), accept_readiness, Rect2(0.44, 0.85, 0.29, 0.14), String(formation.get("status", "draft")) == "ready", _tr("冻结四人顺序;不会选择风险或开始战斗", "Freeze the four-member order; no risk or battle starts"), true) var risk := _readiness_master_command_button("ReadinessRisk", READINESS_COMMAND_RISK, _tr("比较风险", "RISK"), open_risk_board, Rect2(0.75, 0.85, 0.21, 0.14), String(formation.get("status", "draft")) != "ready", _tr("接受准备后比较三个风险选项", "Compare three risk options after preparation is accepted"), false) for command in [revise, back, accept, risk]: command.z_index = 50 overlay.add_child(command) var focus_nodes: Array[Control] = [settings_button] focus_nodes.append_array(member_nodes) focus_nodes.append(warning_button) focus_nodes.append(revise) focus_nodes.append(back) focus_nodes.append(accept) focus_nodes.append(risk) for index in range(focus_nodes.size()): var current := focus_nodes[index] var next := focus_nodes[(index + 1) % focus_nodes.size()] var previous := focus_nodes[(index - 1 + focus_nodes.size()) % focus_nodes.size()] current.focus_next = current.get_path_to(next) current.focus_previous = current.get_path_to(previous) var initial_focus: Control = accept if not accept.disabled else revise initial_focus.call_deferred("grab_focus") func _readiness_master_label(parent: Control, node_name: String, area: Rect2, text_value: String, font_size: int, color: Color) -> Label: var label := _label(text_value, _readiness_master_text_size(font_size), color) label.name = node_name label.set_meta("design_font_size", font_size) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING label.mouse_filter = Control.MOUSE_FILTER_IGNORE if node_name in ["ReadinessFrozenFacts", "ReadinessProtection", "ReadinessReach", "ReadinessPressureCoverage", "ReadinessReserve"]: label.z_index = 21 _party_master_place(label, area) parent.add_child(label) return label func _readiness_master_text_size(design_size: int) -> int: var canvas_height := readiness_master_design_canvas.size.y if is_instance_valid(readiness_master_design_canvas) else size.y var canvas_scale := clampf(canvas_height / 360.0, 1.0, 2.0) var text_scale := float(setup_draft.get("text_scale", settings.get("text_scale", 1.0))) return mini(16, maxi(design_size, int(round(float(design_size) * canvas_scale * text_scale)))) func _readiness_master_place(control: Control, area: Rect2) -> void: _party_master_place(control, area) func _readiness_master_action_button(node_name: String, area: Rect2, callback: Callable, tooltip: String) -> Button: var button := Button.new() button.name = node_name button.flat = true button.focus_mode = Control.FOCUS_ALL button.tooltip_text = tooltip button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) _party_master_place(button, area) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _readiness_master_member_button(position: int, recruit: Dictionary, area: Rect2) -> Button: var recruit_id := String(recruit.get("recruit_id", "")) var button := _readiness_master_action_button("ReadinessPosition%d" % position, area, _readiness_review_member.bind(recruit_id), _tr("查看 %s 的准备事实", "Review %s readiness facts") % _recruit_name(recruit)) button.set_meta("recruit_id", recruit_id) button.set_meta("position", position) button.clip_contents = false button.add_theme_stylebox_override("focus", _flat_style(Color(0.95, 0.82, 0.34, 0.11), Color("#d6ad5d"), 2, 8)) 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.20 portrait.anchor_top = -0.22 portrait.anchor_right = 1.20 portrait.anchor_bottom = 0.93 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(portrait) return button func _readiness_warning_button(formation: Dictionary) -> Button: var button := Button.new() button.name = "ReadinessWarning" button.flat = true button.focus_mode = Control.FOCUS_ALL button.tooltip_text = _tr("查看允许的警告与归属", "Review allowed warnings and ownership") for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) button.pressed.connect(_readiness_review_warnings, CONNECT_DEFERRED) var lines: Array[String] = [] var coverage: Dictionary = formation.get("coverage", {}) if int(coverage.get("protection", 0)) == 0: lines.append(_tr("无保护职责", "NO GUARD")) elif int(coverage.get("long_reach", 0)) == 0: lines.append(_tr("无整线射程", "NO REACH")) elif int(coverage.get("sustain", 0)) == 0: lines.append(_tr("无续航职责", "NO SUSTAIN")) else: lines.append(_tr("覆盖完整", "COVERAGE OK")) lines.append(_tr("结果不保证", "Outcome not guaranteed")) lines.append(_tr("预备席:无", "Reserve: none")) for index in range(3): var row := Control.new() row.name = "ReadinessWarningRow%d" % index row.anchor_left = 0.05 row.anchor_right = 0.95 row.anchor_top = 0.02 + float(index) * 0.32 row.anchor_bottom = 0.30 + float(index) * 0.32 row.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(row) var accent := ColorRect.new() accent.anchor_right = 0.025 accent.anchor_bottom = 1.0 accent.color = [Color("#b7584d"), Color("#d6ad5d"), Color("#4b8790")][index] accent.mouse_filter = Control.MOUSE_FILTER_IGNORE row.add_child(accent) var label := _label(lines[index], _readiness_master_text_size(8), Color("#514331")) label.name = "WarningLabel" label.anchor_left = 0.07 label.anchor_right = 1.0 label.anchor_bottom = 1.0 label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING label.mouse_filter = Control.MOUSE_FILTER_IGNORE row.add_child(label) return button func _readiness_master_command_button(node_name: String, texture: Texture2D, text_value: String, callback: Callable, area: Rect2, disabled: bool, tooltip: String, primary: bool) -> Button: var button := Button.new() button.name = node_name button.flat = true button.focus_mode = Control.FOCUS_ALL button.disabled = disabled button.tooltip_text = tooltip button.mouse_default_cursor_shape = Control.CURSOR_FORBIDDEN if disabled else Control.CURSOR_POINTING_HAND var normal := _flat_style(Color("#17323d", 1.0) if not primary else Color("#3d6934", 1.0), Color("#d6ad5d", 0.92), 2, 7) var hover := _flat_style(Color("#295264", 1.0) if not primary else Color("#4d8341", 1.0), Color("#f3d991"), 2, 7) var disabled_style := _flat_style(Color("#38454a", 0.72), Color("#788187", 0.60), 2, 7) button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("hover", hover) button.add_theme_stylebox_override("pressed", hover) button.add_theme_stylebox_override("focus", hover) button.add_theme_stylebox_override("hover_pressed", hover) button.add_theme_stylebox_override("disabled", disabled_style) _party_master_place(button, area) var icon := TextureRect.new() icon.name = "CommandIcon" icon.texture = texture icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS icon.anchor_left = 0.04 icon.anchor_top = 0.06 icon.anchor_right = 0.34 icon.anchor_bottom = 0.94 icon.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(icon) var label := _button_label("ButtonLabel", text_value, _readiness_master_text_size(10 if primary else 9), Color("#f5ead0") if not disabled else Color("#a7afb0")) label.anchor_left = 0.31 label.anchor_top = 0.10 label.anchor_right = 0.98 label.anchor_bottom = 0.92 label.offset_left = 0 label.offset_top = 0 label.offset_right = 0 label.offset_bottom = 0 label.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING button.add_child(label) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _readiness_review_member(recruit_id: String) -> void: party_notice = _tr("已查看 %s 的冻结身份;队列顺序不会在准备检查中改变。", "Reviewed %s's frozen identity; the order does not change in Readiness Review.") % _recruit_name(_recruit_by_id(guild_company, recruit_id)) show_readiness_review(guild_company) func _readiness_review_warnings() -> void: party_notice = _tr("警告只来自已保存的覆盖事实;允许的警告不会阻止接受准备。", "Warnings come from saved coverage facts; allowed warnings do not block preparation.") show_readiness_review(guild_company) func _readiness_open_settings() -> void: settings_caller = "readiness" show_settings() 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_risk_board_master_base() _build_risk_board_master_ui(resolved, formation) func _build_risk_board_master_base() -> void: for child in get_children(): child.free() var matte := ColorRect.new() matte.name = "RiskBoardMatte" matte.color = Color("#16252d") matte.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) matte.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(matte) risk_board_master_design_canvas = Control.new() risk_board_master_design_canvas.name = "RiskBoardMasterCanvas" risk_board_master_design_canvas.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(risk_board_master_design_canvas) _layout_risk_board_master_canvas() var master := TextureRect.new() master.name = "RiskBoardStaticMaster" master.texture = RISK_BOARD_STATIC_MASTER master.expand_mode = TextureRect.EXPAND_IGNORE_SIZE master.stretch_mode = TextureRect.STRETCH_SCALE master.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) master.mouse_filter = Control.MOUSE_FILTER_IGNORE risk_board_master_design_canvas.add_child(master) func _layout_risk_board_master_canvas() -> void: if not is_instance_valid(risk_board_master_design_canvas): return var available := size if available.x <= 0.0 or available.y <= 0.0: return var canvas_size := Vector2(available.y * 16.0 / 9.0, available.y) if available.x / available.y >= 16.0 / 9.0 else Vector2(available.x, available.x * 9.0 / 16.0) risk_board_master_design_canvas.position = (available - canvas_size) * 0.5 risk_board_master_design_canvas.size = canvas_size func _risk_master_place(control: Control, area: Rect2) -> void: _party_master_place(control, area) func _risk_master_label(parent: Control, node_name: String, area: Rect2, value: String, font_size: int, color: Color, align := HORIZONTAL_ALIGNMENT_LEFT) -> Label: var label := _label(value, _risk_master_text_size(font_size), color) label.name = node_name label.horizontal_alignment = align label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING label.mouse_filter = Control.MOUSE_FILTER_IGNORE _risk_master_place(label, area) parent.add_child(label) return label func _risk_master_text_size(design_size: int) -> int: var canvas_height := risk_board_master_design_canvas.size.y if is_instance_valid(risk_board_master_design_canvas) else size.y var scale := clampf(canvas_height / 360.0, 1.0, 2.0) * float(setup_draft.get("text_scale", settings.get("text_scale", 1.0))) return mini(18, maxi(design_size, int(round(float(design_size) * scale)))) func _risk_master_action_button(node_name: String, area: Rect2, callback: Callable, tooltip: String) -> Button: var button := Button.new() button.name = node_name button.flat = false button.focus_mode = Control.FOCUS_ALL button.tooltip_text = tooltip button.text = tooltip button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button.add_theme_font_override("font", UI_FONT) button.add_theme_font_size_override("font_size", _risk_master_text_size(9)) button.add_theme_color_override("font_color", Color("#f5ead0")) button.add_theme_color_override("font_hover_color", Color("#fff1ba")) button.add_theme_stylebox_override("normal", _flat_style(Color("#17323d", 0.95), Color("#d6ad5d", 0.9), 2, 8)) button.add_theme_stylebox_override("hover", _flat_style(Color("#295264", 0.98), Color("#f3d991"), 2, 8)) button.add_theme_stylebox_override("pressed", _flat_style(Color("#4b6f50", 0.98), Color("#f3d991"), 2, 8)) _risk_master_place(button, area) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _risk_open_settings() -> void: settings_caller = "risk_board" show_settings() func _build_risk_board_master_ui(resolved: Dictionary, formation: Dictionary) -> void: var canvas := risk_board_master_design_canvas var overlay := Control.new() overlay.name = "RiskBoardMasterOverlay" overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) overlay.mouse_filter = Control.MOUSE_FILTER_PASS canvas.add_child(overlay) _risk_master_label(overlay, "RiskTitle", Rect2(0.07, 0.035, 0.19, 0.045), _tr("RSK-001 风险委员会", "RSK-001 RISK BOARD"), 9, Color("#f5ead0")) _risk_master_label(overlay, "RiskSubtitle", Rect2(0.07, 0.082, 0.19, 0.035), _tr("风险比较", "COMPARE RISK"), 7, Color("#e8d39d")) _risk_master_label(overlay, "RiskSnapshot", Rect2(0.28, 0.03, 0.30, 0.05), _tr("队伍快照 · 四人已冻结", "PARTY SNAPSHOT · FOUR FROZEN"), 10, Color("#2b241b")) var party_ids: Array = Array(formation.get("accepted_party_snapshot", {}).get("front_to_rear_ids", formation.get("saved_front_to_rear_ids", []))) for visual_index in range(party_ids.size()): var recruit := _recruit_by_id(resolved, String(party_ids[party_ids.size() - 1 - visual_index])) var actor := TextureRect.new() actor.name = "RiskPartyActor%d" % visual_index actor.texture = InitialCompanyArt.texture_for(recruit) actor.expand_mode = TextureRect.EXPAND_IGNORE_SIZE actor.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED actor.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS _risk_master_place(actor, Rect2(0.28 + float(visual_index) * 0.13, 0.055, 0.10, 0.13)) actor.mouse_filter = Control.MOUSE_FILTER_IGNORE overlay.add_child(actor) _risk_master_label(overlay, "RiskPartyPosition%d" % visual_index, Rect2(0.365 + float(visual_index) * 0.13, 0.075, 0.075, 0.045), _tr("位置 %d", "POS %d") % (party_ids.size() - 1 - visual_index), 7, Color("#2b241b")) var options: Array = formation.get("risk_options", []) var columns := [Rect2(0.085, 0.22, 0.265, 0.57), Rect2(0.367, 0.22, 0.265, 0.57), Rect2(0.649, 0.22, 0.265, 0.57)] var focus_nodes: Array[Control] = [] for index in range(mini(options.size(), 3)): var option: Dictionary = options[index] var button := _risk_master_option_button(option, columns[index]) overlay.add_child(button) focus_nodes.append(button) var back := _risk_master_action_button("RiskBackReadiness", Rect2(0.03, 0.84, 0.18, 0.14), back_to_guild, _tr("返回公会", "Back to Guild")) var review := _risk_master_action_button("RiskReview", Rect2(0.37, 0.84, 0.26, 0.14), open_risk_commitment, _tr("检查已选风险", "Review selected risk")) review.disabled = risk_selected_id.is_empty() review.add_theme_stylebox_override("disabled", _flat_style(Color("#51575b", 0.92), Color("#8b8e8e"), 2, 8)) var settings_button := _risk_master_action_button("RiskSettings", Rect2(0.79, 0.84, 0.18, 0.14), _risk_open_settings, _tr("打开设置", "Open settings")) overlay.add_child(back) overlay.add_child(review) overlay.add_child(settings_button) # Keep the selection explanation in the clear strip between the option band # and command rail. The authored rail begins at 0.84, so the helper must not # be anchored below it where it can be clipped or hidden by the buttons. var no_selection := _risk_master_label(overlay, "RiskNoSelection", Rect2(0.37, 0.785, 0.26, 0.036), _tr("请选择一个风险以查看详情", "SELECT ONE RISK TO ENABLE REVIEW"), 7, Color("#2b241b"), HORIZONTAL_ALIGNMENT_CENTER) no_selection.visible = risk_selected_id.is_empty() var selected_detail := _risk_master_label(overlay, "RiskSelectedDetail", Rect2(0.37, 0.785, 0.26, 0.036), "", 7, Color("#3d7449"), HORIZONTAL_ALIGNMENT_CENTER) selected_detail.visible = not risk_selected_id.is_empty() if not risk_selected_id.is_empty(): selected_detail.text = _tr("已选:%s · 再次点击可更换", "Selected: %s · click another to change") % _risk_title(_risk_option_by_id(formation, risk_selected_id)) # The option rows already carry the sourced coverage facts. Retain this # semantic node for accessibility/state inspection, but do not paint a # redundant summary over the command rail at compact landscape sizes. var coverage := _risk_master_label(overlay, "RiskCoverage", Rect2(0.38, 0.785, 0.24, 0.036), _tr("覆盖:保护 %d · 射程 %d · 续航 %d · 控制 %d", "Coverage: Guard %d · Reach %d · Sustain %d · Control %d") % [int(formation.coverage.get("protection", 0)), int(formation.coverage.get("long_reach", 0)), int(formation.coverage.get("sustain", 0)), int(formation.coverage.get("control", 0))], 7, Color("#2b241b"), HORIZONTAL_ALIGNMENT_CENTER) coverage.visible = false coverage.set_meta("semantic_node_retained", true) focus_nodes.append(back) focus_nodes.append(review) focus_nodes.append(settings_button) for index in range(focus_nodes.size()): focus_nodes[index].focus_next = focus_nodes[index].get_path_to(focus_nodes[(index + 1) % focus_nodes.size()]) focus_nodes[index].focus_previous = focus_nodes[index].get_path_to(focus_nodes[(index - 1 + focus_nodes.size()) % focus_nodes.size()]) (focus_nodes[0] if focus_nodes.size() > 0 else review).call_deferred("grab_focus") func _risk_master_option_button(option: Dictionary, area: Rect2) -> Button: var option_id := String(option.get("risk_option_id", "")) var button := _risk_master_action_button("RiskOption%s" % option_id, area, select_risk_option.bind(option_id), _risk_title(option)) button.text = "" button.set_meta("selected", option_id == risk_selected_id) button.add_theme_stylebox_override("normal", StyleBoxEmpty.new()) button.add_theme_stylebox_override("hover", _flat_style(Color("#f2ca75", 0.12), Color(0, 0, 0, 0), 0, 0)) button.add_theme_stylebox_override("pressed", _flat_style(Color("#5d855e", 0.16), Color(0, 0, 0, 0), 0, 0)) # The clean master intentionally leaves the option interiors neutral. Cover # that region with an opaque warm-paper surface so the authored panel frame # remains visible while no baked semantic glyphs can leak through. var paper := ColorRect.new() paper.name = "RiskOptionPaper" paper.color = Color("#f2eadc") # Draw the paper below the Button's theme so hover/pressed/focus states # remain visible while the surface still sits above the static master. paper.show_behind_parent = true paper.mouse_filter = Control.MOUSE_FILTER_IGNORE _risk_master_place(paper, Rect2(-0.02, 0.14, 1.02, 0.84)) button.add_child(paper) var selection := ColorRect.new() selection.name = "RiskSelectionHighlight" selection.color = Color("#d6ad5d", 0.18) selection.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) selection.mouse_filter = Control.MOUSE_FILTER_IGNORE selection.visible = option_id == risk_selected_id button.add_child(selection) var letter := _risk_master_label(button, "RiskOptionLetter", Rect2(0.05, 0.02, 0.16, 0.12), ["A", "B", "C"][int(option.get("risk", 0))], 16, Color("#263546"), HORIZONTAL_ALIGNMENT_CENTER) letter.add_theme_color_override("font_shadow_color", Color("#f4e6be")) _risk_master_label(button, "RiskOptionTitle", Rect2(0.22, 0.035, 0.73, 0.08), _risk_title(option), 11, Color("#2b241b"), HORIZONTAL_ALIGNMENT_CENTER) var lines := [ _tr("威胁 %d人 · %d波", "THREAT %d · %d WAVES") % [int(option.known_threat.enemy_count), int(option.known_threat.wave_count)], _tr("信息 %s", "INFO %s") % _risk_uncertainty_short(String(option.uncertainty_id)), _tr("成本 %d金", "COST %d COIN") % int(option.entry_cost), _tr("奖励 %s", "REWARD %s") % _risk_reward_short(String(option.reward_class_id)), _tr("恢复 %s", "RECOVERY %s") % _recovery_name(String(option.recovery_exposure_id)), _tr("覆盖 %d/4", "COVERAGE %d/4") % int(option.get("risk", 0) + 2), ] var fact_symbols := ["!", "?", "¢", "◆", "!", "●"] var fact_colors := [Color("#a74337"), Color("#5d3f91"), Color("#a07816"), Color("#3d7449"), Color("#a44a2e"), Color("#2c67a3")] for index in range(lines.size()): var icon := Panel.new() icon.name = "RiskFactIcon%d" % index var icon_style := StyleBoxFlat.new() icon_style.bg_color = fact_colors[index] icon_style.border_color = Color("#f1e6c9") icon_style.set_border_width_all(1) icon_style.set_corner_radius_all(12) icon.add_theme_stylebox_override("panel", icon_style) _risk_master_place(icon, Rect2(0.045, 0.19 + float(index) * 0.125, 0.09, 0.085)) icon.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(icon) var glyph := _risk_master_label(icon, "Glyph", Rect2(0, 0, 1, 1), fact_symbols[index], 8, Color("#fff7dc"), HORIZONTAL_ALIGNMENT_CENTER) glyph.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _risk_master_label(button, "RiskOptionFact%d" % index, Rect2(0.16, 0.18 + float(index) * 0.125, 0.78, 0.105), lines[index], 8, Color("#2b241b")) return button 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" var snapshot: Dictionary = formation.get("accepted_party_snapshot", {}) var snapshot_hash := String(snapshot.get("snapshot_hash", "")) var option_hash := String(option.get("option_hash", "")) var contract_hash := String(resolved.active_contract.get("contract_hash", "")) var binding_changed := risk_commitment_bound_risk_id != risk_selected_id or risk_commitment_bound_snapshot_hash != snapshot_hash or risk_commitment_bound_option_hash != option_hash or risk_commitment_bound_contract_hash != contract_hash if risk_commitment_bound_risk_id.is_empty(): risk_commitment_bound_risk_id = risk_selected_id risk_commitment_bound_snapshot_hash = snapshot_hash risk_commitment_bound_option_hash = option_hash risk_commitment_bound_contract_hash = contract_hash elif binding_changed: risk_commitment_acknowledged = false risk_commitment_notice = _tr("队伍或风险投影已变化,请重新确认。", "The Party or risk projection changed; confirm again.") if binding_changed: risk_commitment_bound_risk_id = risk_selected_id risk_commitment_bound_snapshot_hash = snapshot_hash risk_commitment_bound_option_hash = option_hash risk_commitment_bound_contract_hash = contract_hash _build_risk_commitment_master_base() _build_risk_commitment_master_ui(resolved, formation, option) func _build_risk_commitment_master_base() -> void: for child in get_children(): child.free() var matte := ColorRect.new() matte.name = "RiskCommitmentMatte" matte.color = Color("#16252d") matte.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) matte.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(matte) risk_commitment_master_design_canvas = Control.new() risk_commitment_master_design_canvas.name = "RiskCommitmentMasterCanvas" risk_commitment_master_design_canvas.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(risk_commitment_master_design_canvas) _layout_risk_commitment_master_canvas() var master := TextureRect.new() master.name = "RiskCommitmentStaticMaster" master.texture = RISK_COMMITMENT_STATIC_MASTER master.expand_mode = TextureRect.EXPAND_IGNORE_SIZE master.stretch_mode = TextureRect.STRETCH_SCALE master.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS master.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) master.mouse_filter = Control.MOUSE_FILTER_IGNORE risk_commitment_master_design_canvas.add_child(master) func _layout_risk_commitment_master_canvas() -> void: if not is_instance_valid(risk_commitment_master_design_canvas): return var available := size if available.x <= 0.0 or available.y <= 0.0: return var canvas_size := Vector2(available.y * 16.0 / 9.0, available.y) if available.x / available.y >= 16.0 / 9.0 else Vector2(available.x, available.x * 9.0 / 16.0) risk_commitment_master_design_canvas.position = (available - canvas_size) * 0.5 risk_commitment_master_design_canvas.size = canvas_size func _risk_commitment_text_size(design_size: int) -> int: var canvas_height := risk_commitment_master_design_canvas.size.y if is_instance_valid(risk_commitment_master_design_canvas) else size.y var canvas_scale := clampf(canvas_height / 360.0, 1.0, 2.0) var text_scale := float(setup_draft.get("text_scale", settings.get("text_scale", 1.0))) return mini(17, maxi(7, int(round(float(design_size) * lerpf(1.0, canvas_scale, 0.35) * text_scale)))) func _risk_commitment_place(control: Control, area: Rect2) -> void: _party_master_place(control, area) func _risk_commitment_label(parent: Control, node_name: String, area: Rect2, value: String, font_size: int, color: Color, align := HORIZONTAL_ALIGNMENT_LEFT) -> Label: var label := _label(value, _risk_commitment_text_size(font_size), color) label.name = node_name label.horizontal_alignment = align label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING label.mouse_filter = Control.MOUSE_FILTER_IGNORE _risk_commitment_place(label, area) parent.add_child(label) return label func _risk_commitment_hit_button(node_name: String, area: Rect2, callback: Callable, tooltip: String, disabled := false) -> Button: var button := Button.new() button.name = node_name button.flat = true button.focus_mode = Control.FOCUS_ALL button.tooltip_text = tooltip button.disabled = disabled button.mouse_default_cursor_shape = Control.CURSOR_FORBIDDEN if disabled else Control.CURSOR_POINTING_HAND for style_name in ["normal", "disabled"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) button.add_theme_stylebox_override("hover", _flat_style(Color(0.95, 0.82, 0.35, 0.08), Color(0.95, 0.82, 0.35, 0.45), 1, 5)) button.add_theme_stylebox_override("pressed", _flat_style(Color(0.28, 0.53, 0.38, 0.16), Color(0.95, 0.82, 0.35, 0.62), 2, 5)) button.add_theme_stylebox_override("focus", _flat_style(Color(0.22, 0.45, 0.57, 0.13), Color(0.55, 0.84, 0.94, 0.92), 2, 5)) _risk_commitment_place(button, area) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _risk_commitment_command_button(node_name: String, area: Rect2, text_value: String, glyph: String, callback: Callable, primary := false, disabled := false) -> Button: var button := _risk_commitment_hit_button(node_name, area, callback, text_value, disabled) button.add_theme_stylebox_override("hover", _flat_style(Color(0.95, 0.82, 0.35, 0.12), Color("#f3d991"), 2, 6)) button.add_theme_stylebox_override("pressed", _flat_style(Color(0.28, 0.53, 0.38, 0.24), Color("#f3d991"), 2, 6)) button.add_theme_stylebox_override("disabled", _flat_style(Color(0.12, 0.16, 0.18, 0.22), Color(0.48, 0.50, 0.48, 0.34), 1, 6)) var icon := _risk_commitment_label(button, "CommandGlyph", Rect2(0.06, 0.18, 0.22, 0.64), glyph, 15 if primary else 12, Color("#f3d991") if not disabled else Color("#89939a"), HORIZONTAL_ALIGNMENT_CENTER) icon.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.65)) icon.add_theme_constant_override("shadow_offset_x", 1) icon.add_theme_constant_override("shadow_offset_y", 1) var label := _risk_commitment_label(button, "ButtonLabel", Rect2(0.27, 0.16, 0.68, 0.68), text_value, 10 if primary else 8, Color("#fff4d2") if not disabled else Color("#89939a"), HORIZONTAL_ALIGNMENT_CENTER) label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS if text_value.is_empty(): icon.anchor_left = 0.12 icon.anchor_top = 0.08 icon.anchor_right = 0.88 icon.anchor_bottom = 0.92 label.visible = false button.set_meta("risk_commitment_primary", primary) return button func _risk_commitment_marker(parent: Control, node_name: String, area: Rect2, glyph: String, color: Color, text_value := "") -> Control: var marker := Control.new() marker.name = node_name marker.mouse_filter = Control.MOUSE_FILTER_IGNORE _risk_commitment_place(marker, area) var seal := Panel.new() seal.name = "MarkerSeal" seal.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) seal.mouse_filter = Control.MOUSE_FILTER_IGNORE var style := StyleBoxFlat.new() style.bg_color = color style.border_color = Color("#f5e8c4") style.set_border_width_all(1) style.set_corner_radius_all(10) seal.add_theme_stylebox_override("panel", style) marker.add_child(seal) _risk_commitment_label(marker, "MarkerGlyph", Rect2(0.0, 0.0, 1.0, 1.0), glyph, 8, Color("#fff7dc"), HORIZONTAL_ALIGNMENT_CENTER) if not text_value.is_empty(): var text := _risk_commitment_label(marker, "MarkerText", Rect2(1.05, 0.0, 3.8, 1.0), text_value, 7, Color("#3a3025")) text.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT parent.add_child(marker) return marker func _build_risk_commitment_master_ui(resolved: Dictionary, formation: Dictionary, option: Dictionary) -> void: var canvas := risk_commitment_master_design_canvas var overlay := Control.new() overlay.name = "RiskCommitmentMasterOverlay" overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) overlay.mouse_filter = Control.MOUSE_FILTER_PASS canvas.add_child(overlay) var contract: Dictionary = resolved.active_contract var snapshot: Dictionary = formation.get("accepted_party_snapshot", {}) var party_ids: Array = Array(snapshot.get("front_to_rear_ids", formation.get("saved_front_to_rear_ids", []))) var dependency := _risk_commitment_dependency_status(resolved, formation, option) var ready := risk_commitment_acknowledged and bool(dependency.get("ok", false)) _risk_commitment_label(overlay, "RiskCommitmentPageTitle", Rect2(0.08, 0.018, 0.27, 0.052), _tr("风险承诺", "RISK COMMITMENT"), 12, Color("#fff1c2")) _risk_commitment_label(overlay, "RiskCommitmentHeaderRule", Rect2(0.08, 0.068, 0.30, 0.032), _tr("在进入战斗前核对一条已选路线。", "VERIFY ONE SELECTED ROUTE BEFORE BATTLE."), 7, Color("#e8d39d")) var settings_button := _risk_commitment_command_button("RiskCommitmentSettings", Rect2(0.90, 0.0, 0.10, 0.14), "", "⚙", _risk_commitment_open_settings, false) settings_button.tooltip_text = _tr("打开设置", "Open settings") overlay.add_child(settings_button) # The left relic is a single selected-risk surface. Values are short, sourced # facts so the painted parchment remains visible and never becomes a card wall. var risk_glyphs := ["◆", "▲", "✦"] _risk_commitment_marker(overlay, "RiskIdentityMarker", Rect2(0.085, 0.105, 0.055, 0.072), risk_glyphs[clampi(int(option.get("risk", 0)), 0, 2)], [Color("#3d7449"), Color("#a07816"), Color("#a74337")][clampi(int(option.get("risk", 0)), 0, 2)]) var identity := _risk_commitment_label(overlay, "RiskSelectedIdentity", Rect2(0.16, 0.105, 0.20, 0.048), _risk_title(option), 9, Color("#2d241a"), HORIZONTAL_ALIGNMENT_RIGHT) identity.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS var source := _risk_commitment_label(overlay, "RiskSelectedSource", Rect2(0.16, 0.152, 0.20, 0.034), _tr("来源 %s", "SOURCE %s") % String(option.get("risk_option_id", "")).substr(0, 8), 5, Color("#6e5634"), HORIZONTAL_ALIGNMENT_RIGHT) source.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS var contract_id := String(contract.get("contract_id", "CTR-R1-01")) _risk_commitment_label(overlay, "RiskSelectedRegion", Rect2(0.10, 0.53, 0.16, 0.04), _tr("区域 %s", "REGION %s") % contract_id, 6, Color("#3d3124"), HORIZONTAL_ALIGNMENT_CENTER) var slot_labels := [ [_tr("威胁", "THREAT"), _tr("%d人 · %d波", "%d · %d WAVES") % [int(option.known_threat.enemy_count), int(option.known_threat.wave_count)], "!", Color("#a74337")], [_tr("信息", "INFO"), _risk_uncertainty_short(String(option.uncertainty_id)), "?", Color("#5d3f91")], [_tr("入场", "ENTRY"), _tr("%d 金", "%d COIN") % int(option.entry_cost), "¢", Color("#a07816")], [_tr("回报", "REWARD"), _tr("%d · %s", "%d · %s") % [int(option.reward_coin), _risk_reward_short(String(option.reward_class_id))], "◆", Color("#3d7449")], [_tr("恢复", "RECOVERY"), _recovery_name(String(option.recovery_exposure_id)), "!", Color("#a44a2e")], [_tr("撤退", "RETREAT"), _tr("下阶段归队", "NEXT STAGE"), "↩", Color("#2c67a3")], ] for index in range(slot_labels.size()): var y := 0.115 + float(index) * 0.073 _risk_commitment_marker(overlay, "RiskSlotMarker%d" % index, Rect2(0.265, y + 0.004, 0.026, 0.038), String(slot_labels[index][2]), slot_labels[index][3]) var slot_label := _risk_commitment_label(overlay, "RiskSlotLabel%d" % index, Rect2(0.296, y, 0.095, 0.020), String(slot_labels[index][0]), 5, Color("#766043"), HORIZONTAL_ALIGNMENT_CENTER) slot_label.visible = false slot_label.set_meta("semantic_node_retained", true) var slot_value := _risk_commitment_label(overlay, "RiskSlotValue%d" % index, Rect2(0.292, y + 0.006, 0.101, 0.050), String(slot_labels[index][1]), 7, Color("#31271c"), HORIZONTAL_ALIGNMENT_CENTER) slot_value.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS var detail := _risk_commitment_label(overlay, "RiskCommitmentDetail", Rect2(0.08, 0.575, 0.31, 0.045), _risk_detail_copy(option), 6, Color("#5c4930")) detail.visible = false detail.set_meta("semantic_node_retained", true) var recovery_rule := _risk_commitment_label(overlay, "RiskRecoveryRule", Rect2(0.08, 0.615, 0.31, 0.03), _tr("击倒成员仅离开本场;下阶段以同一身份归队。", "A knocked-out member leaves this battle only and returns next stage with the same identity."), 6, Color("#3d7449")) recovery_rule.visible = false recovery_rule.set_meta("semantic_node_retained", true) # Independent hit areas follow the handoff contract and stay transparent over # the painted relic and ledger shell. var selected_hit := _risk_commitment_hit_button("RiskSelectedRiskDetails", Rect2(0.055, 0.055, 0.36, 0.585), review_risk_commitment_details, _tr("查看已选风险事实", "Review selected risk facts")) selected_hit.set_meta("hit_area_id", "selected_risk_details") overlay.add_child(selected_hit) var consequence_hit := _risk_commitment_hit_button("RiskConsequenceBand", Rect2(0.075, 0.65, 0.85, 0.145), review_risk_commitment_consequences, _tr("查看六条后果事实", "Review six consequence facts")) consequence_hit.set_meta("hit_area_id", "consequence_band") overlay.add_child(consequence_hit) # Re-add the semantic labels after the transparent hit areas so the labels # remain visually above the interaction layer. var fact_root := Control.new() fact_root.name = "RiskCommitmentFacts" fact_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) fact_root.mouse_filter = Control.MOUSE_FILTER_IGNORE overlay.add_child(fact_root) var facts := [ [_tr("威胁", "THREAT"), _tr("%d人 · %d波", "%d · %d WAVES") % [int(option.known_threat.enemy_count), int(option.known_threat.wave_count)], "!", Color("#a74337")], [_tr("不确定", "UNCERTAINTY"), _risk_uncertainty_short(String(option.uncertainty_id)), "?", Color("#5d3f91")], [_tr("入场 Coin", "ENTRY COIN"), _tr("%d · 余额 %d", "%d · %d LEFT") % [int(option.entry_cost), int(contract.get("coin", 0))], "¢", Color("#a07816")], [_tr("胜利回报", "REWARD"), _tr("%d · %s", "%d · %s") % [int(option.reward_coin), _risk_reward_short(String(option.reward_class_id))], "◆", Color("#3d7449")], [_tr("恢复暴露", "RECOVERY"), _recovery_name(String(option.recovery_exposure_id)), "!", Color("#a44a2e")], [_tr("撤退规则", "RETREAT"), _tr("下阶段归队", "NEXT STAGE"), "↩", Color("#2c67a3")], ] for index in range(facts.size()): var fact := Control.new() fact.name = "RiskConsequenceFact%d" % index _risk_commitment_place(fact, Rect2(0.085 + float(index) * 0.139, 0.668, 0.128, 0.12)) fact_root.add_child(fact) _risk_commitment_marker(fact, "FactMarker", Rect2(0.01, 0.08, 0.16, 0.32), String(facts[index][2]), facts[index][3]) _risk_commitment_label(fact, "FactLabel", Rect2(0.20, 0.03, 0.78, 0.32), String(facts[index][0]), 7, Color("#766043"), HORIZONTAL_ALIGNMENT_CENTER) _risk_commitment_label(fact, "FactValue", Rect2(0.18, 0.37, 0.80, 0.57), String(facts[index][1]), 7, Color("#31271c"), HORIZONTAL_ALIGNMENT_CENTER) var consequence_summary := _risk_commitment_label(overlay, "RiskConsequenceSummary", Rect2(0.09, 0.785, 0.82, 0.025), _tr("六条事实均来自当前风险投影;没有隐藏胜率或自动推荐。", "Six facts come from the current risk projection; no hidden win rate or recommendation."), 6, Color("#3e3428"), HORIZONTAL_ALIGNMENT_CENTER) consequence_summary.set_meta("semantic_node_retained", true) # Party staging is one non-interactive parent with four complete portraits in # the accepted rear-left to front-right presentation order. var party_root := Control.new() party_root.name = "RiskFrozenMembers" _risk_commitment_place(party_root, Rect2(0.47, 0.235, 0.49, 0.40)) party_root.mouse_filter = Control.MOUSE_FILTER_IGNORE overlay.add_child(party_root) _risk_commitment_label(overlay, "RiskPartySnapshotTitle", Rect2(0.48, 0.075, 0.25, 0.04), _tr("冻结队伍 · 四人", "FROZEN PARTY · FOUR"), 9, Color("#2d241a")) var snapshot_version := String(snapshot.get("snapshot_id", snapshot.get("snapshot_hash", ""))).substr(0, 10) _risk_commitment_label(overlay, "RiskPartySnapshotVersion", Rect2(0.80, 0.055, 0.145, 0.04), _tr("快照 %s", "SNAP %s") % snapshot_version, 7, Color("#463725"), HORIZONTAL_ALIGNMENT_RIGHT) for visual_index in range(party_ids.size() - 1, -1, -1): var position_index := visual_index var display_index := party_ids.size() - 1 - visual_index var recruit_id := String(party_ids[visual_index]) var recruit := _recruit_by_id(resolved, recruit_id) var member := Control.new() member.name = "RiskFrozenMember%d" % display_index _risk_commitment_place(member, Rect2(float(display_index) * 0.245, 0.0, 0.225, 1.0)) member.mouse_filter = Control.MOUSE_FILTER_IGNORE member.set_meta("recruit_id", recruit_id) member.set_meta("position_index", position_index) party_root.add_child(member) var actor := TextureRect.new() actor.name = "RecruitPortrait" actor.texture = InitialCompanyArt.texture_for(recruit) actor.expand_mode = TextureRect.EXPAND_IGNORE_SIZE actor.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED actor.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS _risk_commitment_place(actor, Rect2(0.02, 0.02, 0.96, 0.72)) actor.mouse_filter = Control.MOUSE_FILTER_IGNORE member.add_child(actor) var relation := _risk_commitment_relation_for(formation, recruit_id) var exposure := _risk_commitment_exposure_text(String(relation.get("exposure_id", "rear"))) var marker := _risk_commitment_marker(member, "PartyFactMarker", Rect2(0.28, 0.10, 0.16, 0.14), "●", Color("#3d7449")) marker.set_meta("reach_id", String(relation.get("reach_id", "limited"))) _risk_commitment_label(member, "PartyPosition", Rect2(0.02, 0.70, 0.96, 0.12), _tr("位置 %d · %s", "POS %d · %s") % [position_index, exposure], 7, Color("#3a3025"), HORIZONTAL_ALIGNMENT_CENTER) _risk_commitment_label(member, "PartyIdentity", Rect2(0.0, 0.81, 1.0, 0.19), _tr("%s\n%s", "%s\n%s") % [_recruit_name(recruit), _recruit_profession_name(recruit)], 7, Color("#2e271e"), HORIZONTAL_ALIGNMENT_CENTER) _risk_commitment_label(overlay, "RiskPartyCoverage", Rect2(0.49, 0.505, 0.44, 0.035), _tr("覆盖 · 保护 %d 远程 %d 续航 %d 控制 %d", "COVERAGE · GUARD %d REACH %d SUSTAIN %d CONTROL %d") % [int(formation.coverage.get("protection", 0)), int(formation.coverage.get("long_reach", 0)), int(formation.coverage.get("sustain", 0)), int(formation.coverage.get("control", 0))], 6, Color("#3e3428"), HORIZONTAL_ALIGNMENT_CENTER) # The command rail remains a transparent semantic overlay on the authored # dark rail. Begin Battle is disabled until acknowledgement and dependency # validation both succeed. var cancel := _risk_commitment_command_button("RiskCommitmentCancel", Rect2(0.03, 0.83, 0.19, 0.15), _tr("返回风险板", "RISK BOARD"), "‹", cancel_risk_commitment, false) overlay.add_child(cancel) var acknowledge := _risk_commitment_command_button("RiskCommitmentAcknowledge", Rect2(0.40, 0.83, 0.20, 0.15), _tr("确认", "ACK") if risk_commitment_acknowledged else _tr("确认", "ACK"), "✓" if risk_commitment_acknowledged else "!", toggle_risk_commitment_confirmation, true) acknowledge.set_meta("confirmed", risk_commitment_acknowledged) overlay.add_child(acknowledge) var begin := _risk_commitment_command_button("RiskBeginBattle", Rect2(0.78, 0.83, 0.19, 0.15), _tr("开始战斗", "BEGIN BATTLE"), "➜", begin_battle, true, not ready) begin.tooltip_text = _tr("确认后创建一次承诺回执并进入战斗开场。", "After acknowledgement, create one commitment receipt and enter Battle Opening.") if not begin.disabled else _risk_commitment_block_reason(dependency, risk_commitment_acknowledged) _set_risk_commitment_button_disabled(begin, not ready) overlay.add_child(begin) # Historical DEV-4 callers inspect this node to verify the pre-commit boundary. # Keep it as a non-rendered disabled alias while the real command owns the slot. var unavailable := Button.new() unavailable.name = "RiskBeginBattleUnavailable" unavailable.visible = false unavailable.disabled = true unavailable.mouse_filter = Control.MOUSE_FILTER_IGNORE unavailable.set_meta("semantic_alias", "RiskBeginBattle") overlay.add_child(unavailable) var summary := _risk_commitment_label(overlay, "RiskCommitmentSummary", Rect2(0.36, 0.855, 0.28, 0.028), _tr("一次回执", "ONE RECEIPT"), 5, Color("#ead8ad"), HORIZONTAL_ALIGNMENT_CENTER) summary.mouse_filter = Control.MOUSE_FILTER_IGNORE var reason_text := risk_commitment_notice if reason_text.is_empty(): reason_text = _tr("先确认后果;确认前不改变金币或队伍。", "Acknowledge first; Coin and Party stay unchanged.") if not risk_commitment_acknowledged else _tr("已确认:下一步进入战斗开场。", "Acknowledged: next step opens Battle Opening.") if not bool(dependency.get("ok", false)): reason_text = _risk_commitment_block_reason(dependency, risk_commitment_acknowledged) var reason := _risk_commitment_label(overlay, "RiskBeginBattleReason", Rect2(0.34, 0.925, 0.32, 0.038), reason_text, 6, Color("#f0ca75") if not ready else Color("#9ed7aa"), HORIZONTAL_ALIGNMENT_CENTER) reason.mouse_filter = Control.MOUSE_FILTER_IGNORE var focus_nodes: Array[Control] = [settings_button, selected_hit, consequence_hit, acknowledge, begin, cancel] for index in range(focus_nodes.size()): focus_nodes[index].focus_next = focus_nodes[index].get_path_to(focus_nodes[(index + 1) % focus_nodes.size()]) focus_nodes[index].focus_previous = focus_nodes[index].get_path_to(focus_nodes[(index - 1 + focus_nodes.size()) % focus_nodes.size()]) settings_button.call_deferred("grab_focus") func _risk_commitment_relation_for(formation: Dictionary, recruit_id: String) -> Dictionary: for relation in formation.get("relations", []): if String(relation.get("recruit_id", "")) == recruit_id: return Dictionary(relation) return {} func _risk_commitment_exposure_text(value: String) -> String: match value: "highest": return _tr("最前", "FRONT") "high": return _tr("前列", "HIGH") "guarded": return _tr("受护", "GUARDED") return _tr("后列", "REAR") func _risk_commitment_dependency_status(company: Dictionary, formation: Dictionary, option: Dictionary) -> Dictionary: if formation.is_empty() or String(formation.get("status", "")) != "ready": return {"ok": false, "code": "READINESS_REQUIRED"} var snapshot: Dictionary = formation.get("accepted_party_snapshot", {}) var ids: Array = Array(snapshot.get("front_to_rear_ids", [])) if ids.size() != 4 or String(snapshot.get("snapshot_hash", "")).is_empty(): return {"ok": false, "code": "PARTY_SNAPSHOT_INVALID"} var seen := {} for recruit_id in ids: var id := String(recruit_id) if id.is_empty() or seen.has(id) or _recruit_by_id(company, id).is_empty(): return {"ok": false, "code": "PARTY_SNAPSHOT_INVALID"} seen[id] = true var current_option := _risk_option_by_id(formation, String(option.get("risk_option_id", ""))) if current_option.is_empty() or String(current_option.get("option_hash", "")) != String(option.get("option_hash", "")): return {"ok": false, "code": "RISK_PROJECTION_STALE"} var contract: Dictionary = company.get("active_contract", {}) var cost := int(option.get("entry_cost", 0)) var coin := int(contract.get("coin", 0)) if coin < cost: return {"ok": false, "code": "INSUFFICIENT_COIN", "coin": coin, "cost": cost} return {"ok": true, "code": "READY", "snapshot_hash": String(snapshot.snapshot_hash), "option_hash": String(option.option_hash), "contract_hash": String(contract.get("contract_hash", ""))} func _risk_commitment_block_reason(dependency: Dictionary, acknowledged: bool) -> String: if not acknowledged: return _tr("请先确认已读六条后果。", "Acknowledge the six consequences first.") match String(dependency.get("code", "")): "READINESS_REQUIRED": return _tr("准备快照不可用,请返回准备检查。", "The readiness snapshot is unavailable; return to Readiness Review.") "PARTY_SNAPSHOT_INVALID": return _tr("队伍快照无效,请返回准备检查。", "The Party snapshot is invalid; return to Readiness Review.") "RISK_PROJECTION_STALE": return _tr("风险投影已失效,请返回风险板。", "The risk projection is stale; return to the Risk Board.") "INSUFFICIENT_COIN": return _tr("金币不足:需要 %d,当前 %d。", "Insufficient Coin: need %d, have %d.") % [int(dependency.get("cost", 0)), int(dependency.get("coin", 0))] return _tr("依赖检查未完成。", "Dependency checks are not complete.") func _set_risk_commitment_button_disabled(button: Button, disabled: bool) -> void: button.disabled = disabled button.mouse_default_cursor_shape = Control.CURSOR_FORBIDDEN if disabled else Control.CURSOR_POINTING_HAND var label := button.get_node_or_null("ButtonLabel") as Label var icon := button.get_node_or_null("CommandGlyph") as Label if label != null: label.add_theme_color_override("font_color", Color("#89939a") if disabled else Color("#fff4d2")) if icon != null: icon.add_theme_color_override("font_color", Color("#89939a") if disabled else Color("#f3d991")) func _risk_commitment_open_settings() -> void: settings_caller = "risk_commitment" show_settings() func review_risk_commitment_details() -> void: risk_commitment_notice = _tr("风险身份、来源和六个值均来自当前投影。", "Identity, source and six values come from the current projection.") var reason := find_child("RiskBeginBattleReason", true, false) as Label if reason != null: reason.text = risk_commitment_notice func review_risk_commitment_consequences() -> void: risk_commitment_notice = _tr("后果顺序:威胁、不确定、入场、回报、恢复、撤退。", "Order: threat, uncertainty, entry, reward, recovery, retreat.") var reason := find_child("RiskBeginBattleReason", true, false) as Label if reason != null: reason.text = risk_commitment_notice func toggle_risk_commitment_confirmation() -> void: risk_commitment_acknowledged = not risk_commitment_acknowledged risk_commitment_notice = _tr("已确认六条后果;现在可以进入战斗开场。", "Six consequences acknowledged; Battle Opening is now available.") if risk_commitment_acknowledged else _tr("确认已撤回;不会创建承诺回执。", "Acknowledgement withdrawn; no commitment receipt will be created.") show_risk_commitment(guild_company) func back_to_readiness() -> void: show_readiness_review(guild_company) func cancel_risk_commitment() -> void: risk_commitment_acknowledged = false risk_commitment_notice = "" risk_commitment_receipt = {} show_risk_board(guild_company) func begin_battle() -> void: if risk_selected_id.is_empty(): show_risk_board(guild_company) return if not risk_commitment_acknowledged: risk_commitment_notice = _tr("请先确认已读后果。", "Acknowledge the consequences before beginning battle.") show_risk_commitment(guild_company) return formation_risk_service.company_path = initial_company_service.company_path var formation_loaded: Dictionary = formation_risk_service.load_company() if not formation_loaded.ok: party_notice = _formation_error_text(String(formation_loaded.get("code", "RISK_LOAD_FAILED"))) show_readiness_review(guild_company) return var validated_company: Dictionary = formation_loaded.company var validated_formation: Dictionary = validated_company.active_contract.get("formation", {}) var validated_option := _risk_option_by_id(validated_formation, risk_selected_id) var dependency := _risk_commitment_dependency_status(validated_company, validated_formation, validated_option) if not bool(dependency.get("ok", false)): risk_commitment_notice = _risk_commitment_block_reason(dependency, true) show_risk_commitment(validated_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 snapshot_hash := String(Dictionary(loaded.company.active_contract.get("formation", {}).get("accepted_party_snapshot", {})).get("snapshot_hash", "")) var operation_id := ("%s|battle|%s|%s" % [String(loaded.company.company_id), risk_selected_id, snapshot_hash]).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 risk_commitment_receipt = Dictionary(started.get("receipt", {})).duplicate(true) battle_operation_id = operation_id battle_paused = false battle_speed = 1.0 battle_notice = "" guild_company = started.company.duplicate(true) show_battle_opening(guild_company) func show_battle_opening(company := {}, auto_focus := true) -> void: _clear_battle_playback_timer() battle_playback_generation += 1 battle_opening_mode = true var preserve_handoff := battle_opening_state == "opening_handoff" and not battle_opening_handoff_receipt.is_empty() battle_opening_notice = "" if not preserve_handoff: battle_opening_state = "ready_to_observe" battle_opening_handoff_receipt = {} 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 := battle_service.load_company() if not loaded.ok: _show_battle_opening_invalid(_battle_error_text(String(loaded.get("code", "BATTLE_LOAD_FAILED")))) return resolved = loaded.company.duplicate(true) var contract: Dictionary = resolved.get("active_contract", {}) var battle: Dictionary = contract.get("battle_state", {}) if battle.is_empty() or String(battle.get("status", "")) not in ["active"]: _show_battle_opening_invalid(_tr("没有可恢复的战斗开场回执。", "No resumable Battle Opening receipt exists.")) return if int(battle.get("phase_index", 0)) > 0: battle_opening_state = "interrupted_resume" battle_opening_notice = _tr("已恢复原战斗回执;请重新确认后再交给观察页。", "The original battle receipt was restored; confirm the opening again before entering observation.") battle_operation_id = String(battle.get("operation_id", "")) guild_company = resolved # A resumed/loaded opening may not have the transient RSK-002 UI receipt in # memory. Resolve the persisted commitment identity from the same contract # so the opening never renders a blank or fabricated receipt slot. if risk_commitment_receipt.is_empty(): # The current Company schema persists the battle receipt, while the # commitment review object is intentionally transient. Reuse the linked # operation identity rather than inventing a second commitment record. var persisted_battles: Array = Array(contract.get("battle_receipts", [])) for persisted in persisted_battles: if String(persisted.get("operation_id", "")) == battle_operation_id: risk_commitment_receipt = Dictionary(persisted).duplicate(true) break current_screen = "battle" _build_battle_opening_canvas() _build_battle_opening_overlay(resolved, battle) if auto_focus: var start := find_child("BattleStartObservation", true, false) as Button if start != null and not start.disabled: start.call_deferred("grab_focus") func _show_battle_opening_invalid(reason: String) -> void: battle_opening_mode = true battle_opening_state = "no_resumable_receipt" battle_opening_notice = reason current_screen = "battle" _build_battle_opening_canvas() var canvas := battle_opening_canvas var box := Control.new() box.name = "BattleOpeningInvalid" box.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) canvas.add_child(box) var title := _battle_opening_overlay_label(box, "BattleOpeningInvalidTitle", _tr("战斗开场不可用", "BATTLE OPENING UNAVAILABLE"), Rect2(0.20, 0.36, 0.60, 0.07), 24, Color("#f5ead0")) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER var detail := _battle_opening_overlay_label(box, "BattleOpeningInvalidReason", reason, Rect2(0.18, 0.45, 0.64, 0.10), 14, Color("#d5c8aa")) detail.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART var back := _battle_opening_button(box, "BattleOpeningInvalidBack", _tr("返回风险承诺", "Return to Risk Commitment"), Rect2(0.38, 0.66, 0.24, 0.14), cancel_risk_commitment, false) back.call_deferred("grab_focus") func _build_battle_opening_canvas() -> void: for child in get_children(): child.free() var background := TextureRect.new() background.name = "BattleOpeningWorld" background.texture = BATTLE_OPENING_BACKGROUND background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED background.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS 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 = "BattleOpeningWorldShade" shade.color = Color(0.02, 0.05, 0.08, 0.08) shade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) shade.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(shade) var canvas := Control.new() canvas.name = "BattleOpeningCanvas" canvas.mouse_filter = Control.MOUSE_FILTER_PASS add_child(canvas) battle_opening_canvas = canvas _layout_battle_opening_canvas() func _layout_battle_opening_canvas() -> void: if not is_instance_valid(battle_opening_canvas): return var available := size if available.x <= 0.0 or available.y <= 0.0: return const DESIGN_ASPECT := 16.0 / 9.0 var canvas_size: Vector2 if available.x / available.y >= DESIGN_ASPECT: canvas_size = Vector2(available.y * DESIGN_ASPECT, available.y) else: canvas_size = Vector2(available.x, available.x / DESIGN_ASPECT) battle_opening_canvas.position = (available - canvas_size) * 0.5 battle_opening_canvas.size = canvas_size func _build_battle_opening_overlay(company: Dictionary, battle: Dictionary) -> void: if not is_instance_valid(battle_opening_canvas): return var art := BattleOpeningArt.new() art.name = "BattleOpeningArt" art.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) art.set_state(true, battle_opening_state == "opening_handoff") battle_opening_canvas.add_child(art) var overlay := Control.new() overlay.name = "BattleOpeningLayout" overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) overlay.mouse_filter = Control.MOUSE_FILTER_PASS battle_opening_canvas.add_child(overlay) _build_battle_opening_top(overlay, company, battle) _build_battle_opening_field(overlay, company, battle) _build_battle_opening_bottom(overlay, company, battle) func _build_battle_opening_top(parent: Control, company: Dictionary, battle: Dictionary) -> void: var party_count := Array(battle.get("front_to_rear_ids", [])).size() var enemy_count := Array(battle.get("enemy_ids", [])).size() var company_status := _battle_opening_overlay_label(parent, "BattleCompanyStatus", _tr("远征团 %d", "COMPANY %d") % party_count, Rect2(0.105, 0.030, 0.20, 0.055), 16, Color("#f5ead0")) company_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT var enemy_status := _battle_opening_overlay_label(parent, "BattleEnemyStatus", _tr("敌方 %d", "ENEMY %d") % enemy_count, Rect2(0.735, 0.030, 0.18, 0.055), 16, Color("#f5ead0")) enemy_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT var pause := _battle_opening_button(parent, "BattlePause", "II", Rect2(0.405, 0.005, 0.065, 0.14), review_battle_opening_state, false) pause.tooltip_text = _tr("战斗时间线将在下一页开始", "The battle timeline begins on the next page") var speed := _battle_opening_button(parent, "BattleSpeed", "x1", Rect2(0.468, 0.005, 0.065, 0.14), review_battle_speed, false) speed.tooltip_text = _tr("观察速度由 BAT-004 管理", "Observation speed is owned by BAT-004") var inspect := _battle_opening_button(parent, "BattleInspect", "VIEW", Rect2(0.531, 0.005, 0.065, 0.14), inspect_battle_opening, false) inspect.tooltip_text = _tr("检查开场事实", "Inspect opening facts") var settings_button := _battle_opening_button(parent, "BattleOpeningSettings", _tr("设置", "SETTINGS"), Rect2(0.905, 0.005, 0.09, 0.14), _battle_open_settings, false) settings_button.add_theme_font_size_override("font_size", _scaled(10)) func _build_battle_opening_field(parent: Control, company: Dictionary, battle: Dictionary) -> void: var objective_title := _battle_opening_overlay_label(parent, "BattleOpeningTitle", _tr("风镜初渡 · 战斗开场", "FIRST WINDGLASS CROSSING · BATTLE OPENING"), Rect2(0.045, 0.165, 0.28, 0.035), 13, Color("#e8d39d")) objective_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT var objective := _battle_opening_overlay_label(parent, "BattleOpeningObjective", _tr("目标:击破敌方队列", "OBJECTIVE: BREAK THE ENEMY LINE"), Rect2(0.045, 0.205, 0.28, 0.055), 15, Color("#f5ead0")) objective.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT var commitment := _battle_opening_overlay_label(parent, "BattleOpeningCommitment", _tr("承诺回执 %s", "COMMITMENT %s") % _short_hash(risk_commitment_receipt.get("receipt_hash", "")), Rect2(0.045, 0.255, 0.28, 0.030), 9, Color("#b8d6e8")) commitment.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT var no_respawn := _battle_opening_overlay_label(parent, "BattleOpeningNoRespawn", _tr("无战斗内复活", "NO COMBAT RESPAWN"), Rect2(0.045, 0.305, 0.28, 0.035), 11, Color("#f0c979")) no_respawn.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT var no_reserve := _battle_opening_overlay_label(parent, "BattleOpeningNoReserve", _tr("无 Reserve 自动入场", "NO RESERVE AUTO-ENTRY"), Rect2(0.045, 0.350, 0.28, 0.035), 11, Color("#f0c979")) no_reserve.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT var actors := Control.new() actors.name = "BattleActors" actors.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) actors.mouse_filter = Control.MOUSE_FILTER_IGNORE parent.add_child(actors) var ids: Array = Array(battle.get("front_to_rear_ids", [])) var visual_slots := [Rect2(0.075, 0.455, 0.115, 0.235), Rect2(0.185, 0.445, 0.115, 0.245), Rect2(0.295, 0.435, 0.115, 0.255), Rect2(0.405, 0.425, 0.115, 0.265)] for visual_index in range(ids.size()): var source_index := ids.size() - 1 - visual_index var recruit_id := String(ids[source_index]) var recruit := _recruit_by_id(company, recruit_id) var rect: Rect2 = visual_slots[mini(visual_index, visual_slots.size() - 1)] var actor := _battle_opening_actor(source_index, recruit, battle, rect) actors.add_child(actor) var enemies := Control.new() enemies.name = "BattleEnemies" enemies.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) enemies.mouse_filter = Control.MOUSE_FILTER_IGNORE parent.add_child(enemies) var enemy_ids: Array = Array(battle.get("enemy_ids", [])) var enemy_slots := [Rect2(0.625, 0.475, 0.105, 0.22), Rect2(0.735, 0.455, 0.105, 0.24), Rect2(0.845, 0.435, 0.105, 0.26)] for enemy_index in range(enemy_ids.size()): var enemy_rect: Rect2 = enemy_slots[mini(enemy_index, enemy_slots.size() - 1)] enemies.add_child(_battle_opening_enemy(enemy_index, String(enemy_ids[enemy_index]), battle, enemy_rect)) var orientation := _battle_opening_overlay_label(parent, "BattleOpeningOrientation", _tr("后排在左 · 0 号前排在右", "REAR LEFT · FRONT POSITION 0 RIGHT"), Rect2(0.075, 0.705, 0.30, 0.035), 9, Color("#c7ddb0")) orientation.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT var enemy_direction := _battle_opening_overlay_label(parent, "BattleOpeningEnemyDirection", _tr("敌方朝向 ◀ 接触走廊", "ENEMY FACING ◀ CONTACT CORRIDOR"), Rect2(0.62, 0.705, 0.30, 0.035), 9, Color("#e9b8a3")) enemy_direction.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT var threat_label := _battle_opening_overlay_label(parent, "BattleOpeningFirstThreat", _tr("首要威胁", "FIRST THREAT"), Rect2(0.535, 0.235, 0.12, 0.030), 9, Color("#ffe7bf")) threat_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER var threat_value := _battle_opening_overlay_label(parent, "BattleOpeningThreatValue", _first_battle_threat_copy(battle), Rect2(0.505, 0.270, 0.18, 0.035), 8, Color("#fff1db")) threat_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER threat_value.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART var corridor := _battle_opening_overlay_label(parent, "BattleOpeningCorridor", _tr("移动走廊", "MOVEMENT CORRIDOR"), Rect2(0.355, 0.565, 0.21, 0.035), 9, Color("#f7d88b")) corridor.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER var state_reason := _battle_opening_overlay_label(parent, "BattleOpeningReason", battle_opening_notice, Rect2(0.34, 0.735, 0.32, 0.045), 10, Color("#e8d39d")) state_reason.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER state_reason.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART state_reason.visible = not battle_opening_notice.is_empty() var context := _battle_opening_overlay_label(parent, "BattleEventContext", _tr("开场事实已锁定 · 尚未开始时间线", "OPENING FACTS LOCKED · TIMELINE NOT STARTED"), Rect2(0.34, 0.695, 0.32, 0.035), 8, Color("#d2e0dd")) context.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER func _build_battle_opening_bottom(parent: Control, company: Dictionary, battle: Dictionary) -> void: var snapshot_title := _battle_opening_overlay_label(parent, "BattleOpeningSnapshot", _tr("冻结队列", "FROZEN COMPANY"), Rect2(0.175, 0.845, 0.14, 0.030), 10, Color("#e8d39d")) snapshot_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT var snapshot_hash := _battle_opening_overlay_label(parent, "BattleOpeningSnapshotHash", _short_hash(battle.get("snapshot_hash", "")), Rect2(0.46, 0.845, 0.30, 0.030), 9, Color("#9ec9dd")) snapshot_hash.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT var ids: Array = Array(battle.get("front_to_rear_ids", [])) for visual_index in range(ids.size()): var source_index := ids.size() - 1 - visual_index var recruit := _recruit_by_id(company, String(ids[source_index])) var cell := _battle_opening_snapshot_cell(source_index, recruit, Rect2(0.17 + visual_index * 0.155, 0.875, 0.145, 0.095)) parent.add_child(cell) var inspect := _battle_opening_button(parent, "BattleInspectOpening", _tr("检查开场", "INSPECT"), Rect2(0.025, 0.835, 0.145, 0.145), inspect_battle_opening, false) inspect.tooltip_text = _tr("检查承诺、顺序和首要威胁", "Inspect commitment, order and first threat") var start_disabled := battle_opening_state == "opening_handoff" var start_text := _tr("已交接", "HANDED OFF") if start_disabled else _tr("开始观察", "START OBSERVATION") var start := _battle_opening_button(parent, "BattleStartObservation", start_text, Rect2(0.805, 0.835, 0.17, 0.145), start_observation_handoff, true) start.disabled = start_disabled start.tooltip_text = _tr("交给 BAT-002;不会在此页推进时间", "Hand off to BAT-002; this page never advances time") if start_disabled: _set_battle_opening_button_disabled(start, true) var committed_rule := _battle_opening_overlay_label(parent, "BattleCommittedRule", _tr("风险已提交 · 无战斗指令", "RISK COMMITTED · NO COMBAT COMMANDS"), Rect2(0.655, 0.945, 0.30, 0.035), 8, Color("#d8c79f")) committed_rule.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT var handoff := _battle_opening_overlay_label(parent, "BattleOpeningHandoff", _tr("开场回执已交给 BAT-002", "OPENING RECEIPT HANDED TO BAT-002"), Rect2(0.36, 0.775, 0.28, 0.055), 10, Color("#b9e4bf")) handoff.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER handoff.visible = battle_opening_state == "opening_handoff" var receipt := _battle_opening_overlay_label(parent, "BattleOpeningReceipt", _tr("战斗回执 %s", "BATTLE RECEIPT %s") % _short_hash(_battle_receipt_hash(company, battle)), Rect2(0.36, 0.815, 0.28, 0.030), 9, Color("#d6e8ed")) receipt.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER receipt.visible = battle_opening_state == "opening_handoff" var next := _battle_opening_overlay_label(parent, "BattleOpeningNextBoundary", _tr("下一页负责确定时序;本页不模拟", "The next page owns the timeline; this page does not simulate"), Rect2(0.34, 0.945, 0.32, 0.035), 8, Color("#b6c8ce")) next.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER next.visible = battle_opening_state == "opening_handoff" func _battle_opening_actor(position: int, recruit: Dictionary, battle: Dictionary, rect: Rect2) -> Control: var actor := Control.new() actor.name = "BattleActor%d" % position _set_normalized_rect(actor, rect) actor.mouse_filter = Control.MOUSE_FILTER_IGNORE actor.set_meta("position", position) actor.set_meta("recruit_id", String(recruit.get("recruit_id", ""))) var shadow := ColorRect.new() shadow.name = "BattleActorGround" shadow.anchor_left = 0.16 shadow.anchor_top = 0.83 shadow.anchor_right = 0.84 shadow.anchor_bottom = 0.88 shadow.color = Color("#10211f", 0.35) shadow.mouse_filter = Control.MOUSE_FILTER_IGNORE actor.add_child(shadow) var portrait := TextureRect.new() portrait.name = "BattlePortrait" portrait.texture = _battle_opening_actor_texture(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.04 portrait.anchor_top = 0.0 portrait.anchor_right = 0.96 portrait.anchor_bottom = 0.84 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE actor.add_child(portrait) var bar := ColorRect.new() bar.name = "BattleActorHP" bar.anchor_left = 0.13 bar.anchor_top = 0.79 bar.anchor_right = 0.87 bar.anchor_bottom = 0.825 bar.color = Color("#69b76b") bar.mouse_filter = Control.MOUSE_FILTER_IGNORE actor.add_child(bar) var pos_label := _label(_tr("%d 号", "POS %d") % position, _scaled(8), Color("#f5e6b8")) pos_label.name = "BattlePositionLabel" pos_label.anchor_left = 0.05 pos_label.anchor_top = 0.875 pos_label.anchor_right = 0.95 pos_label.anchor_bottom = 0.94 pos_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER pos_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER pos_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS pos_label.mouse_filter = Control.MOUSE_FILTER_IGNORE actor.add_child(pos_label) var name_label := _label(_recruit_name(recruit), _scaled(8), Color("#ffffff")) name_label.name = "BattleActorName" name_label.anchor_left = 0.02 name_label.anchor_top = 0.935 name_label.anchor_right = 0.98 name_label.anchor_bottom = 1.0 name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE actor.add_child(name_label) return actor func _battle_opening_enemy(index: int, enemy_id: String, battle: Dictionary, rect: Rect2) -> Control: var enemy := Control.new() enemy.name = "BattleEnemy%d" % index _set_normalized_rect(enemy, rect) enemy.mouse_filter = Control.MOUSE_FILTER_IGNORE var portrait := TextureRect.new() portrait.name = "BattleEnemyPortrait" portrait.texture = BATTLE_OPENING_ENEMY 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.82 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE enemy.add_child(portrait) var bar := ColorRect.new() bar.name = "BattleEnemyHP" bar.anchor_left = 0.12 bar.anchor_top = 0.78 bar.anchor_right = 0.88 bar.anchor_bottom = 0.82 bar.color = Color("#d95f59") if index == 0 else Color("#a94b54") bar.mouse_filter = Control.MOUSE_FILTER_IGNORE enemy.add_child(bar) var label := _label(_tr("威胁 %d", "THREAT %d") % (index + 1), _scaled(8), Color("#ffe5d1")) label.name = "BattleEnemyLabel" label.anchor_top = 0.86 label.anchor_right = 1.0 label.anchor_bottom = 1.0 label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.mouse_filter = Control.MOUSE_FILTER_IGNORE enemy.add_child(label) enemy.set_meta("enemy_id", enemy_id) return enemy func _battle_opening_snapshot_cell(position: int, recruit: Dictionary, rect: Rect2) -> Control: var cell := Control.new() cell.name = "BattleSnapshotCell%d" % position _set_normalized_rect(cell, rect) cell.mouse_filter = Control.MOUSE_FILTER_IGNORE var back := ColorRect.new() back.name = "BattleSnapshotCellBack" back.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) back.color = Color("#24384b", 0.78) back.mouse_filter = Control.MOUSE_FILTER_IGNORE cell.add_child(back) var portrait := TextureRect.new() portrait.texture = _battle_opening_actor_texture(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.02 portrait.anchor_top = 0.02 portrait.anchor_right = 0.28 portrait.anchor_bottom = 0.98 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE cell.add_child(portrait) var label := _label(_tr("%d 号 %s", "POS %d %s") % [position, _recruit_name(recruit)], _scaled(8), Color("#f5ead0")) label.name = "BattleSnapshotIdentity" label.anchor_left = 0.30 label.anchor_top = 0.18 label.anchor_right = 0.97 label.anchor_bottom = 0.62 label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS label.mouse_filter = Control.MOUSE_FILTER_IGNORE cell.add_child(label) var line := ColorRect.new() line.anchor_left = 0.30 line.anchor_top = 0.72 line.anchor_right = 0.94 line.anchor_bottom = 0.79 line.color = Color("#6ebc7a") line.mouse_filter = Control.MOUSE_FILTER_IGNORE cell.add_child(line) return cell func _battle_opening_overlay_label(parent: Control, node_name: String, text_value: String, rect: Rect2, font_size: int, color: Color) -> Label: var label := _label(text_value, _scaled(font_size), color) label.name = node_name _set_normalized_rect(label, rect) label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS label.mouse_filter = Control.MOUSE_FILTER_IGNORE parent.add_child(label) return label func _battle_opening_button(parent: Control, node_name: String, text_value: String, rect: Rect2, callback: Callable, primary: bool) -> Button: var button := Button.new() button.name = node_name button.focus_mode = Control.FOCUS_ALL button.flat = false button.custom_minimum_size = Vector2(48, 48) button.tooltip_text = text_value _set_normalized_rect(button, rect) var normal := _flat_style(Color("#d4b46b", 0.92) if primary else Color("#172536", 0.86), Color("#f1d995", 0.95), 2 if primary else 1, 5) var hover := _flat_style(Color("#ebd28d", 0.96) if primary else Color("#29425b", 0.94), Color("#fff0bf", 1.0), 2, 5) button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("hover", hover) button.add_theme_stylebox_override("pressed", _flat_style(Color("#a98642", 0.96), Color("#fff0bf", 1.0), 2, 5)) button.add_theme_stylebox_override("focus", hover) button.add_theme_stylebox_override("disabled", _flat_style(Color("#26313b", 0.70), Color("#6a7175", 0.65), 1, 5)) button.add_theme_stylebox_override("hover_pressed", hover) var label := _button_label("ButtonLabel", text_value, _scaled(10 if not primary else 12), Color("#17202a") if primary else Color("#f5ead0")) label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS button.add_child(label) button.pressed.connect(callback, CONNECT_DEFERRED) parent.add_child(button) return button func _set_battle_opening_button_disabled(button: Button, disabled: bool) -> void: button.disabled = disabled var label := button.get_node_or_null("ButtonLabel") as Label if label != null: label.add_theme_color_override("font_color", Color("#9aa1a4") if disabled else Color("#17202a")) func _battle_opening_actor_texture(recruit: Dictionary) -> Texture2D: match String(recruit.get("profession_id", "")): "PF-B01", "PF-B02", "PF-B04": return BATTLE_OPENING_HEARTHGUARD "PF-B03", "PF-B09", "PF-B11": return BATTLE_OPENING_WAYRUNNER "PF-B05", "PF-B06", "PF-B10": return BATTLE_OPENING_FARSTRING "PF-B07", "PF-B08", "PF-B12": return BATTLE_OPENING_KINDHAND return InitialCompanyArt.texture_for(recruit) func _first_battle_threat_copy(battle: Dictionary) -> String: var enemy_count := Array(battle.get("enemy_ids", [])).size() return _tr("0 号承压 · %d 敌", "POS 0 EXPOSED · %d ENEMIES") % enemy_count func _battle_receipt_hash(company: Dictionary, battle: Dictionary) -> String: for receipt in company.get("active_contract", {}).get("battle_receipts", []): if String(receipt.get("operation_id", "")) == String(battle.get("operation_id", "")): return String(receipt.get("receipt_hash", "")) return String(battle.get("battle_hash", "")) func _short_hash(value: Variant) -> String: var text := String(value) return text.substr(0, 10) if text.length() > 10 else text func _battle_open_settings() -> void: settings_caller = "battle_opening" show_settings() func review_battle_opening_state() -> void: battle_paused = true battle_opening_notice = _tr("承诺、队列顺序和首要威胁均已冻结;观察尚未开始。", "Commitment, line order and first threat are frozen; observation has not started.") show_battle_opening(guild_company, false) func review_battle_speed() -> void: battle_speed = 2.0 battle_opening_notice = _tr("速度按钮属于 BAT-004;本页只确认开场事实。", "Speed belongs to BAT-004; this page only confirms opening facts.") show_battle_opening(guild_company, false) func inspect_battle_opening() -> void: battle_opening_notice = _tr("同一承诺回执 · 同一 Party 快照 · 无战斗内复活 · 无 Reserve 自动入场。", "Same commitment receipt · same Party snapshot · no combat respawn · no Reserve auto-entry.") show_battle_opening(guild_company, false) func start_observation_handoff() -> void: if not battle_opening_mode or battle_opening_state == "opening_handoff": return battle_service.company_path = initial_company_service.company_path var loaded := battle_service.load_company() if not loaded.ok: battle_opening_state = "snapshot_mismatch" battle_opening_notice = _battle_error_text(String(loaded.get("code", "BATTLE_LOAD_FAILED"))) show_battle_opening(guild_company, false) return var live_company: Dictionary = loaded.company var live_battle: Dictionary = live_company.get("active_contract", {}).get("battle_state", {}) var current_battle: Dictionary = guild_company.get("active_contract", {}).get("battle_state", {}) if String(live_battle.get("operation_id", "")) != String(current_battle.get("operation_id", "")) or String(live_battle.get("snapshot_hash", "")) != String(current_battle.get("snapshot_hash", "")): battle_opening_state = "snapshot_mismatch" battle_opening_notice = _tr("承诺或 Party 快照已变化;观察被阻止。", "The commitment or Party snapshot changed; observation is blocked.") show_battle_opening(live_company, false) return battle_opening_handoff_receipt = _battle_receipt_for_company(live_company, live_battle) battle_opening_state = "opening_handoff" battle_opening_notice = _tr("原战斗回执已交接;BAT-002 负责时序。", "The original battle receipt was handed off; BAT-002 owns the timeline.") guild_company = live_company.duplicate(true) show_battle_opening(guild_company, false) func _battle_receipt_for_company(company: Dictionary, battle: Dictionary) -> Dictionary: for receipt in company.get("active_contract", {}).get("battle_receipts", []): if String(receipt.get("operation_id", "")) == String(battle.get("operation_id", "")): return Dictionary(receipt).duplicate(true) return {} func show_battle(company := {}, auto_play := true) -> void: battle_opening_mode = false battle_opening_state = "" battle_opening_handoff_receipt = {} _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: # The legacy live renderer remains available as BAT-002 provenance. BAT-001 # never calls this path from its explicit opening handoff. battle_opening_mode = false 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(true) var frame_safe := _framed_page_safe("EquipmentReview") frame_safe.add_child(_entry_chrome("EquipmentReviewFrame", "content_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), "content_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(true) var frame_safe := _framed_page_safe("RecruitReview") frame_safe.add_child(_entry_chrome("RecruitReviewFrame", "content_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), "content_primary" if selected else "content_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("#202a31") if selected else 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), "content_primary" if selected else "content_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("#202a31") if selected else 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("#394a52") if selected else Color("#d0d7d5")) 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("#6f562d") if selected else 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, "content_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, "content_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, 150 if size.y < 500.0 else 220) 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), "content_primary" if selected else "content_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("#202a31") if selected else 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.set_meta("position", position) 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 _party_outline_label(label: Label) -> void: label.add_theme_color_override("font_outline_color", Color(0.02, 0.06, 0.08, 0.90)) label.add_theme_constant_override("outline_size", 2) func _party_line_top_rail() -> Control: var rail := Control.new() rail.custom_minimum_size = Vector2(0, 48 if size.y < 500.0 else 62) rail.mouse_filter = Control.MOUSE_FILTER_IGNORE var left := TextureRect.new() left.texture = GUILD_DESK_TOP_LEFT left.expand_mode = TextureRect.EXPAND_IGNORE_SIZE left.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED left.anchor_right = 0.11 left.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE) left.mouse_filter = Control.MOUSE_FILTER_IGNORE rail.add_child(left) var center := TextureRect.new() center.texture = GUILD_DESK_TOP_CENTER center.expand_mode = TextureRect.EXPAND_IGNORE_SIZE center.stretch_mode = TextureRect.STRETCH_SCALE center.anchor_left = 0.09 center.anchor_right = 0.91 center.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE) center.mouse_filter = Control.MOUSE_FILTER_IGNORE rail.add_child(center) var right := TextureRect.new() right.texture = GUILD_DESK_TOP_RIGHT right.expand_mode = TextureRect.EXPAND_IGNORE_SIZE right.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED right.anchor_left = 0.89 right.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE) right.mouse_filter = Control.MOUSE_FILTER_IGNORE rail.add_child(right) var identity := _label(_tr("队列编成", "PARTY LINE"), _scaled(11), Color("#f5ead0")) _party_outline_label(identity) identity.position = Vector2(42, 5) identity.size = Vector2(150, rail.custom_minimum_size.y - 10) identity.vertical_alignment = VERTICAL_ALIGNMENT_CENTER identity.mouse_filter = Control.MOUSE_FILTER_IGNORE rail.add_child(identity) var status := _label(_tr("压力 %s · 远征团 %d/4", "PRESSURE %s · COMPANY %d/4") % [_difficulty_name(String(guild_company.active_contract.get("difficulty", "standard"))), guild_company.owned_recruits.size()], _scaled(9), Color("#f5ead0")) _party_outline_label(status) status.position = Vector2(210, 5) status.size = Vector2(maxf(180.0, size.x - 360.0), rail.custom_minimum_size.y - 10) status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER status.mouse_filter = Control.MOUSE_FILTER_IGNORE rail.add_child(status) var company := _label(_tr("后排在左 · 0 号前排在右", "REAR LEFT · FRONT POSITION 0 RIGHT"), _scaled(9), Color("#f5ead0")) _party_outline_label(company) company.anchor_left = 0.73 company.anchor_right = 0.99 company.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE) company.offset_top = 5 company.offset_bottom = -5 company.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER company.vertical_alignment = VERTICAL_ALIGNMENT_CENTER company.mouse_filter = Control.MOUSE_FILTER_IGNORE rail.add_child(company) # Keep the formation rule available to automation and accessibility without # adding a second visible title to the compact rail. var orientation := Label.new() orientation.name = "PartyOrientation" orientation.text = _tr("后排在左 · 0 号前排在右", "REAR LEFT · FRONT POSITION 0 RIGHT") orientation.visible = false orientation.mouse_filter = Control.MOUSE_FILTER_IGNORE rail.add_child(orientation) return rail func _party_command_button(node_name: String, texture: Texture2D, label_text: String, callback: Callable, variant: String) -> Button: var button := Button.new() button.name = node_name button.flat = true button.custom_minimum_size = Vector2(78, 54) button.tooltip_text = label_text 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", "company_primary" if variant == "primary" else "company_utility" if variant == "utility" else "company_secondary", "")) var icon := TextureRect.new() icon.name = "CommandIcon" icon.texture = texture icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS icon.anchor_left = 0.12 icon.anchor_top = 0.06 icon.anchor_right = 0.46 if variant != "primary" else 0.38 icon.anchor_bottom = 0.94 icon.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(icon) var label_color := Color("#202a31") if variant == "utility" else Color("#f5ead0") var label := _button_label("ButtonLabel", label_text, _scaled(11 if variant != "primary" else 12), label_color) label.anchor_left = 0.39 if variant == "primary" else 0.38 label.anchor_right = 0.97 label.offset_left = 0 label.offset_right = 0 label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS button.add_child(label) button.pressed.connect(callback, 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 _risk_uncertainty_short(value: String) -> String: match value: "one_intent_unresolved": return _tr("1项未解析", "1 INTENT OPEN") "elite_response_unresolved": return _tr("精英时机未解析", "ELITE TIMING OPEN") return _tr("全公开", "CLEAR") func _risk_reward_short(value: String) -> String: match value: "enhanced_choice": return _tr("强化选择", "ENHANCED") "elite_cache": return _tr("精英宝箱", "ELITE CACHE") return _tr("标准补给", "STANDARD") 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("#6a4b29"))) 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("#26343a")) 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("#8f5e28") ) 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("#4a5b5d")) 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("#3f765d") ) 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("#3f6d79") ) 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("#6a4b29")) 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_relation_summary_text(company: Dictionary) -> String: var context_id := party_selected_recruit_id if not party_selected_recruit_id.is_empty() else String(party_draft_order[0]) var relation := _party_relation(context_id) var target_copy := _tr("首攻", "TARGET 1") if bool(relation.get("first_threat_target", false)) else _tr("目标 %d", "TARGET %d") % int(relation.get("expected_target_rank", 1)) return _tr("%d 号\n%s", "POS %d\n%s") % [int(relation.get("position", 0)), target_copy] func _party_threat_summary_text(first_threat: Dictionary) -> String: var context_id := party_selected_recruit_id if not party_selected_recruit_id.is_empty() else String(party_draft_order[0]) var relation := _party_relation(context_id) var protected_count: int = relation.get("protected_by_recruit_ids", []).size() var protection_copy := _tr("无保护", "NO GUARD") if protected_count == 0 else _tr("保护 %d", "GUARD %d") % protected_count return "%s\n%s\n%s" % [ _tr("%d 敌 · %d 波", "EN %d · WV %d") % [int(first_threat.get("enemy_count", 0)), int(first_threat.get("wave_count", 0))], protection_copy, _party_relation_reach_short(String(relation.get("reach_id", "limited"))), ] func _party_relation_reach_short(reach_id: String) -> String: match reach_id: "full_line": return _tr("全队射程", "REACH ALL") "front_three": return _tr("射程前 3", "REACH 3") "front_two": return _tr("射程前 2", "REACH 2") return _tr("射程受限", "LIMITED") 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 _set_normalized_rect(control: Control, rect: Rect2) -> void: control.anchor_left = rect.position.x control.anchor_top = rect.position.y control.anchor_right = rect.end.x control.anchor_bottom = rect.end.y control.offset_left = 0 control.offset_top = 0 control.offset_right = 0 control.offset_bottom = 0 func _guild_master_member(index: int, recruit: Dictionary, texture: Texture2D, rect: Rect2) -> Control: var member := Control.new() member.name = "GuildMember_%d" % index _set_normalized_rect(member, rect) member.mouse_filter = Control.MOUSE_FILTER_IGNORE var shadow := ColorRect.new() shadow.name = "GuildMemberGround_%d" % index shadow.anchor_left = 0.16 shadow.anchor_top = 0.86 shadow.anchor_right = 0.84 shadow.anchor_bottom = 0.91 shadow.color = Color(0.02, 0.08, 0.08, 0.28) shadow.mouse_filter = Control.MOUSE_FILTER_IGNORE member.add_child(shadow) var portrait := TextureRect.new() portrait.name = "GuildMemberPortraitTexture_%d" % index portrait.texture = texture if texture != null else 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.offset_bottom = -20 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE member.add_child(portrait) var name := _label(_recruit_name(recruit), _scaled(11), Color("#fff4d6")) name.name = "GuildMemberName_%d" % index 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 name.mouse_filter = Control.MOUSE_FILTER_IGNORE member.add_child(name) return member func _guild_desk_member(index: int, recruit: Dictionary, rect: Rect2) -> Control: var member := Control.new() member.name = "GuildMember_%d" % index _set_normalized_rect(member, rect) member.mouse_filter = Control.MOUSE_FILTER_IGNORE var shadow := ColorRect.new() shadow.name = "GuildMemberGround_%d" % index shadow.anchor_left = 0.18 shadow.anchor_top = 0.88 shadow.anchor_right = 0.82 shadow.anchor_bottom = 0.915 shadow.color = Color(0.02, 0.08, 0.08, 0.24) shadow.mouse_filter = Control.MOUSE_FILTER_IGNORE member.add_child(shadow) 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.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE member.add_child(portrait) return member func _guild_master_kiosk_button(node_name: String, kind: String, title: String, callback: Callable, rect: Rect2) -> Button: var button := Button.new() button.name = node_name button.flat = true button.tooltip_text = title _set_normalized_rect(button, rect) for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) var kiosk := TextureRect.new() kiosk.name = "GuildMasterKioskTexture" kiosk.texture = GUILD_MASTER_KIOSK if kind == "contract" else GUILD_MASTER_COMPASS kiosk.expand_mode = TextureRect.EXPAND_IGNORE_SIZE kiosk.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED kiosk.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS kiosk.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) kiosk.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(kiosk) var marker := Control.new() marker.name = "Guild%sSticker" % kind.capitalize() marker.visible = false button.add_child(marker) var label := _label(title, _scaled(16), Color("#27323a")) label.anchor_left = 0.30 label.anchor_top = 0.43 label.anchor_right = 0.78 label.anchor_bottom = 0.66 label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(label) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _guild_master_hit_button(node_name: String, tooltip: String, callback: Callable, rect: Rect2) -> Button: var button := Button.new() button.name = node_name button.flat = true button.tooltip_text = tooltip _set_normalized_rect(button, rect) for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _guild_master_overlay_plate(parent: Control, node_name: String, rect: Rect2, background: Color, border: Color, border_width: int, radius: int) -> Panel: var plate := Panel.new() plate.name = node_name _set_normalized_rect(plate, rect) plate.mouse_filter = Control.MOUSE_FILTER_IGNORE plate.add_theme_stylebox_override("panel", _flat_style(background, border, border_width, radius)) parent.add_child(plate) return plate func _guild_master_rail_value(parent: Control, node_name: String, rect: Rect2, value: String) -> Label: var plate := _guild_master_overlay_plate(parent, "%sPlate" % node_name, rect, Color(0.025, 0.09, 0.14, 0.97), Color(0.62, 0.46, 0.23, 0.72), 1, 2) var label := _label(value, _scaled(8), Color("#f1d48b")) 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 plate.add_child(label) return label func _guild_master_command_label(parent: Control, node_name: String, rect: Rect2, value: String) -> Label: var plate := _guild_master_overlay_plate(parent, "%sPlate" % node_name, rect, Color(0.025, 0.09, 0.14, 0.94), Color(0.025, 0.09, 0.14, 0.0), 0, 1) var label := _label(value, _scaled(7), Color("#f1d48b")) 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 plate.add_child(label) return label func _guild_member_column(index: int, recruit: Dictionary, texture_override: Texture2D = null) -> Control: var box := Control.new() box.name = "GuildMember_%d" % index box.custom_minimum_size = Vector2(150, 180 if size.y < 500.0 else 230) box.size_flags_horizontal = Control.SIZE_EXPAND_FILL box.size_flags_vertical = Control.SIZE_EXPAND_FILL box.size_flags_stretch_ratio = 1.0 box.mouse_filter = Control.MOUSE_FILTER_IGNORE var member_tag := _label(_tr("远征团成员 %d" % (index + 1), "COMPANY MEMBER %d" % (index + 1)), _scaled(9), Color("#f5ead0")) member_tag.position = Vector2(8, 2) member_tag.size = Vector2(220, 20) member_tag.visible = false member_tag.mouse_filter = Control.MOUSE_FILTER_IGNORE box.add_child(member_tag) var portrait_stage := Control.new() portrait_stage.name = "GuildMemberPortrait_%d" % index portrait_stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) portrait_stage.offset_top = -8 portrait_stage.offset_bottom = -24 portrait_stage.mouse_filter = Control.MOUSE_FILTER_IGNORE var ground := ColorRect.new() ground.name = "GuildMemberGround_%d" % index ground.anchor_left = 0.08 ground.anchor_top = 0.84 ground.anchor_right = 0.92 ground.anchor_bottom = 0.92 ground.color = Color(0.04, 0.13, 0.13, 0.38) ground.mouse_filter = Control.MOUSE_FILTER_IGNORE portrait_stage.add_child(ground) var tint := ColorRect.new() tint.color = Color(InitialCompanyArt.accent_for(recruit), 0.06) 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 = texture_override if texture_override != null else 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.16 portrait.anchor_top = 0.02 portrait.anchor_right = 0.84 portrait.anchor_bottom = 0.96 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(15), Color("#f5ead0")) name.anchor_top = 0.86 name.anchor_right = 1.0 name.anchor_bottom = 0.99 name.offset_left = 8 name.offset_right = -8 name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS name.mouse_filter = Control.MOUSE_FILTER_IGNORE box.add_child(name) var profession := _label("%s · %s" % [String(recruit.profession_name.get(locale, recruit.profession_name.en)), _tr("等级 1", "Level 1")], _scaled(10), Color("#f0ca75")) profession.anchor_top = 0.84 profession.anchor_right = 1.0 profession.anchor_bottom = 0.93 profession.offset_left = 8 profession.offset_right = -8 profession.visible = false profession.mouse_filter = Control.MOUSE_FILTER_IGNORE box.add_child(profession) var trait_label := _label(String(recruit.trait_name.get(locale, recruit.trait_name.en)), _scaled(10), Color("#b9dfc0")) trait_label.anchor_top = 0.91 trait_label.anchor_right = 1.0 trait_label.anchor_bottom = 0.99 trait_label.offset_left = 8 trait_label.offset_right = -8 trait_label.visible = false trait_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS trait_label.mouse_filter = Control.MOUSE_FILTER_IGNORE box.add_child(trait_label) return box func _guild_destination_button(node_name: String, kind: String, title: String, callable: Callable) -> Button: var button := _entry_button_base(node_name, callable, "content_destination") button.custom_minimum_size = Vector2(250, 66) var sticker = GuildDestinationSticker.new().configure(kind) sticker.name = "Guild%sSticker" % kind.capitalize() sticker.position = Vector2(10, 6) sticker.size = Vector2(82, 54) sticker.visible = false button.add_child(sticker) var prop := TextureRect.new() prop.name = "Guild%sProp" % kind.capitalize() prop.texture = GUILD_CONTRACT_BOARD if kind == "contract" else GUILD_ROUTE_MAP prop.expand_mode = TextureRect.EXPAND_IGNORE_SIZE prop.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED prop.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS prop.position = Vector2(8, 4) prop.size = Vector2(88, 58) prop.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(prop) var label := _label(title, _scaled(16), Color("#24313a")) label.position = Vector2(104, 0) label.size = Vector2(maxf(80.0, button.custom_minimum_size.x - 116.0), 66) 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 in ["company_utility", "content_primary", "content_destination"] 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 _guild_anchor_icon_button(node_name: String, tooltip: String, callback: Callable, icon: String) -> Button: var button := Button.new() button.name = node_name button.flat = true button.custom_minimum_size = Vector2(62, 50) button.tooltip_text = tooltip for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) var medallion := TextureRect.new() medallion.name = "GuildMasterMedallion" medallion.texture = GUILD_MASTER_MEDALLION medallion.expand_mode = TextureRect.EXPAND_IGNORE_SIZE medallion.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED medallion.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS medallion.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) medallion.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(medallion) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _guild_anchor_prop_button(node_name: String, kind: String, title: String, callback: Callable) -> Button: var button := Button.new() button.name = node_name button.flat = true button.custom_minimum_size = Vector2(280, 150) button.tooltip_text = title for style_name in ["normal", "hover", "pressed", "disabled", "focus", "hover_pressed"]: button.add_theme_stylebox_override(style_name, StyleBoxEmpty.new()) var kiosk_texture := TextureRect.new() kiosk_texture.name = "GuildMasterKioskTexture" kiosk_texture.texture = GUILD_MASTER_KIOSK if kind == "contract" else GUILD_MASTER_COMPASS kiosk_texture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE kiosk_texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED kiosk_texture.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS kiosk_texture.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) kiosk_texture.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(kiosk_texture) var marker := Control.new() marker.name = "Guild%sSticker" % kind.capitalize() marker.visible = false button.add_child(marker) var prop := TextureRect.new() prop.name = "Guild%sProp" % kind.capitalize() prop.texture = GUILD_CONTRACT_BOARD if kind == "contract" else GUILD_ROUTE_MAP prop.expand_mode = TextureRect.EXPAND_IGNORE_SIZE prop.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED prop.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS prop.position = Vector2(18, 28) prop.size = Vector2(80, 54) prop.visible = kind != "contract" prop.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(prop) var label := _label(title, _scaled(17), Color("#f4e4bd")) label.position = Vector2(84, 76) label.size = Vector2(176, 42) label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(label) 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 = find_child("GuildNotice", true, false) 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) elif settings_caller == "readiness": show_readiness_review(guild_company) elif settings_caller == "risk_board": show_risk_board(guild_company) elif settings_caller == "risk_commitment": show_risk_commitment(guild_company) elif settings_caller == "battle_opening": show_battle_opening(guild_company, false) 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": if battle_opening_mode: review_battle_opening_state() else: 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(content_scene := false) -> void: for child in get_children(): child.free() var background := TextureRect.new() background.name = "WorldBackground" background.texture = CONTENT_BACKGROUND if content_scene else 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.12 if content_scene else 0.34) shade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) shade.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(shade) func _build_party_master_base() -> void: for child in get_children(): child.free() var matte := ColorRect.new() matte.name = "PartyMasterMatte" matte.color = Color("#17252c") matte.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) matte.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(matte) var canvas := Control.new() canvas.name = "PartyMasterCanvas" canvas.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(canvas) party_master_design_canvas = canvas _layout_party_master_canvas() var master := TextureRect.new() master.name = "PartyMasterImage" var source := Image.load_from_file(ProjectSettings.globalize_path(PARTY_LINE_MASTER_PATH)) master.texture = ImageTexture.create_from_image(source) master.expand_mode = TextureRect.EXPAND_IGNORE_SIZE master.stretch_mode = TextureRect.STRETCH_SCALE master.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) master.mouse_filter = Control.MOUSE_FILTER_IGNORE canvas.add_child(master) func _layout_party_master_canvas() -> void: if not is_instance_valid(party_master_design_canvas): return var available := size if available.x <= 0.0 or available.y <= 0.0: return const DESIGN_ASPECT := 16.0 / 9.0 var canvas_size: Vector2 if available.x / available.y >= DESIGN_ASPECT: canvas_size = Vector2(available.y * DESIGN_ASPECT, available.y) else: canvas_size = Vector2(available.x, available.x / DESIGN_ASPECT) party_master_design_canvas.position = (available - canvas_size) * 0.5 party_master_design_canvas.size = canvas_size func _build_party_master_ui(resolved: Dictionary, formation: Dictionary) -> void: var canvas := find_child("PartyMasterCanvas", true, false) as Control if canvas == null: return var overlay := Control.new() overlay.name = "PartyMasterOverlay" overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) overlay.mouse_filter = Control.MOUSE_FILTER_PASS canvas.add_child(overlay) _party_master_label(overlay, "PartyTitle", Rect2(0.205, 0.045, 0.125, 0.05), _tr("队列编成", "PARTY LINE"), _party_master_text_size(10), Color("#242c2b")) _party_master_label( overlay, "PartyPressureValue", Rect2(0.415, 0.045, 0.095, 0.05), _party_pressure_short(String(resolved.active_contract.get("difficulty", "standard"))), _party_master_text_size(10), Color("#514331") ) _party_master_label(overlay, "PartyCompanyValue", Rect2(0.635, 0.045, 0.105, 0.05), "%d/4" % resolved.owned_recruits.size(), _party_master_text_size(10), Color("#242c2b")) var diagram := PartyLineDiagram.new() diagram.name = "PartyOrderDiagram" _party_master_place(diagram, Rect2(0.775, 0.17, 0.185, 0.12)) overlay.add_child(diagram) var cord_track := PartyLineTrack.new() cord_track.name = "PartyFormationCord" cord_track.render_layer = PartyLineTrack.RenderLayer.CORD cord_track.z_index = 25 cord_track.set_meta("render_layer", "cord") _party_master_place(cord_track, Rect2(0.10, 0.27, 0.72, 0.43)) cord_track.mouse_filter = Control.MOUSE_FILTER_IGNORE overlay.add_child(cord_track) var member_areas := [ Rect2(0.590, 0.295, 0.105, 0.375), Rect2(0.460, 0.325, 0.105, 0.345), Rect2(0.330, 0.310, 0.105, 0.360), Rect2(0.200, 0.310, 0.105, 0.360), ] for visual_index in range(party_draft_order.size() - 1, -1, -1): var recruit_id := String(party_draft_order[visual_index]) var recruit := _recruit_by_id(resolved, recruit_id) var button := _party_master_member_button(visual_index, recruit, member_areas[visual_index]) button.z_index = 30 overlay.add_child(button) var member_nodes: Array[Control] = [] for child in overlay.get_children(): if child is Button and String(child.name).begins_with("PartyPosition"): member_nodes.append(child) var effects_track := PartyLineTrack.new() effects_track.name = "PartyFormationEffects" effects_track.render_layer = PartyLineTrack.RenderLayer.EFFECTS effects_track.z_index = 35 effects_track.set_meta("render_layer", "effects") _party_master_place(effects_track, Rect2(0.10, 0.27, 0.72, 0.43)) effects_track.mouse_filter = Control.MOUSE_FILTER_IGNORE effects_track.bind(member_nodes, party_selected_recruit_id, String(settings.get("locale", "zh_CN"))) cord_track.bind(member_nodes, party_selected_recruit_id, String(settings.get("locale", "zh_CN"))) overlay.add_child(effects_track) var relation := _label("", _party_master_text_size(10), Color("#2b241b")) relation.name = "PartyRelationSummary" relation.z_index = 40 relation.set_meta("design_font_size", 10) _party_master_place(relation, Rect2(0.375, 0.665, 0.095, 0.105)) relation.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART relation.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER relation.vertical_alignment = VERTICAL_ALIGNMENT_CENTER relation.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING relation.text = _party_relation_summary_text(resolved) relation.tooltip_text = _recruit_name(_recruit_by_id(resolved, party_selected_recruit_id if not party_selected_recruit_id.is_empty() else String(party_draft_order[0]))) overlay.add_child(relation) var first_threat: Dictionary = formation.get("first_threat", {}) var threat := _label( _party_threat_summary_text(first_threat), _party_master_text_size(10), Color("#2b241b") ) threat.name = "PartyFirstThreat" threat.z_index = 40 threat.set_meta("design_font_size", 10) threat.tooltip_text = _first_threat_text(first_threat) _party_master_place(threat, Rect2(0.49, 0.665, 0.13, 0.105)) threat.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART threat.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER threat.vertical_alignment = VERTICAL_ALIGNMENT_CENTER threat.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING overlay.add_child(threat) var orientation := Label.new() orientation.name = "PartyOrientation" orientation.text = _tr("后排在左 · 0 号前排在右", "REAR LEFT · FRONT POSITION 0 RIGHT") orientation.visible = false overlay.add_child(orientation) var undo := _party_master_command_button( "PartyUndo", _tr("撤销", "Undo"), undo_party_swap, Rect2(0.22, 0.85, 0.12, 0.15), party_undo_order.is_empty(), _tr("还没有可撤销的位置交换", "No position swap to undo") if party_undo_order.is_empty() else _tr("撤销上一次位置交换", "Undo the last position swap"), "secondary" ) overlay.add_child(undo) var reset_disabled: bool = party_draft_order == formation.base_front_to_rear_ids var reset := _party_master_command_button( "PartyReset", _tr("重置", "Reset"), reset_party_order, Rect2(0.36, 0.85, 0.11, 0.15), reset_disabled, _tr("当前已是系统初始顺序", "The automatic starting order is already restored") if reset_disabled else _tr("恢复系统初始顺序", "Restore the automatic starting order"), "secondary" ) overlay.add_child(reset) var save := _party_master_command_button( "PartySaveReview", _tr("保存", "Save"), save_party_order, Rect2(0.48, 0.85, 0.15, 0.15), false, _tr("保存当前顺序并打开准备检查", "Save this order and open Readiness Review"), "primary" ) overlay.add_child(save) var back := _party_master_command_button( "PartyBack", _tr("返回", "Back"), cancel_party_line, Rect2(0.65, 0.85, 0.12, 0.15), false, _tr("取消预览并返回公会", "Cancel the preview and return to Guild"), "secondary" ) overlay.add_child(back) var focus_nodes: Array[Control] = member_nodes.duplicate() focus_nodes.append(undo) focus_nodes.append(reset) focus_nodes.append(save) focus_nodes.append(back) for index in range(focus_nodes.size()): var current := focus_nodes[index] var next := focus_nodes[(index + 1) % focus_nodes.size()] var previous := focus_nodes[(index - 1 + focus_nodes.size()) % focus_nodes.size()] current.focus_next = current.get_path_to(next) current.focus_previous = current.get_path_to(previous) var initial_focus := find_child("PartyPosition3", true, false) as Control if initial_focus != null: initial_focus.call_deferred("grab_focus") func _party_master_member_button(position: int, recruit: Dictionary, area: Rect2) -> Button: var button := Button.new() button.name = "PartyPosition%d" % position button.flat = true button.focus_mode = Control.FOCUS_ALL button.set_meta("recruit_id", String(recruit.get("recruit_id", ""))) button.set_meta("position", position) _party_master_place(button, area) button.tooltip_text = _tr("%d 号位置 · %s · 点选后再点目标位置交换", "Position %d · %s · select, then choose a destination to swap") % [position, _recruit_name(recruit)] button.add_theme_stylebox_override("normal", StyleBoxEmpty.new()) button.add_theme_stylebox_override("hover", _party_master_member_style(true, true)) button.add_theme_stylebox_override("pressed", _party_master_member_style(true, true)) button.add_theme_stylebox_override("focus", _party_master_member_style(true, false)) button.add_theme_stylebox_override("hover_pressed", _party_master_member_style(true, true)) 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.10 portrait.anchor_top = 0.0 portrait.anchor_right = 1.10 portrait.anchor_bottom = 0.90 portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE button.add_child(portrait) button.pressed.connect(select_party_member.bind(String(recruit.get("recruit_id", ""))), CONNECT_DEFERRED) return button func _party_master_command_button(node_name: String, text_value: String, callback: Callable, area: Rect2, disabled: bool, tooltip: String, variant: String = "secondary") -> Button: var button := Button.new() button.name = node_name button.flat = false button.z_index = 40 button.focus_mode = Control.FOCUS_ALL button.disabled = disabled button.set_meta("party_command_role", variant) button.set_meta("party_command_disabled_emphasis", disabled) button.tooltip_text = tooltip _party_master_place(button, area) button.add_theme_stylebox_override("normal", _party_master_command_state_style(false, true, false) if variant == "primary" else StyleBoxEmpty.new()) button.add_theme_stylebox_override("disabled", _party_master_command_state_style(false, false, true)) button.add_theme_stylebox_override("hover", _party_master_command_state_style(false, variant == "primary", false)) button.add_theme_stylebox_override("pressed", _party_master_command_state_style(true, variant == "primary", false)) button.add_theme_stylebox_override("focus", _party_master_command_state_style(false, variant == "primary", false)) button.add_theme_stylebox_override("hover_pressed", _party_master_command_state_style(true, variant == "primary", false)) var label := _button_label("ButtonLabel", text_value, _party_master_text_size(11), Color("#817867") if disabled else Color("#292c27")) label.set_meta("design_font_size", 11) label.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING label.anchor_left = 0.04 label.anchor_top = 0.54 label.anchor_right = 0.96 label.anchor_bottom = 0.99 label.offset_left = 0 label.offset_top = 0 label.offset_right = 0 label.offset_bottom = 0 button.add_child(label) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _party_pressure_short(difficulty: String) -> String: match difficulty: "wayfinder": return _tr("引路", "WAY") "stormbound": return _tr("风暴", "STORM") "iron_oath": return _tr("铁誓", "OATH") return _tr("标准", "STD") func _party_master_label(parent: Control, node_name: String, area: Rect2, text_value: String, font_size: int, color: Color) -> Label: var label := _label(text_value, font_size, color) label.name = node_name 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 _party_master_place(label, area) parent.add_child(label) return label func _party_master_place(control: Control, area: Rect2) -> void: control.anchor_left = area.position.x control.anchor_top = area.position.y control.anchor_right = area.end.x control.anchor_bottom = area.end.y control.offset_left = 0 control.offset_top = 0 control.offset_right = 0 control.offset_bottom = 0 func _party_master_text_size(design_size: int) -> int: var canvas_height := party_master_design_canvas.size.y if is_instance_valid(party_master_design_canvas) else size.y var canvas_scale := clampf(canvas_height / 360.0, 1.0, 2.0) var text_scale := float(setup_draft.get("text_scale", settings.get("text_scale", 1.0))) # The contained canvas can grow to 720p, but the authored parchment slots do # not grow with it. Keep localized copy readable without pushing it outside # the paper plate or command label strip at 130% text scale. return mini(16, maxi(design_size, int(round(float(design_size) * canvas_scale * text_scale)))) func _party_master_member_style(selected: bool, hovered: bool) -> StyleBox: if not selected: return StyleBoxEmpty.new() var style := StyleBoxFlat.new() style.bg_color = Color(0.96, 0.83, 0.35, 0.08 if not hovered else 0.14) style.border_color = Color(1.0, 0.90, 0.48, 0.92) style.set_border_width_all(2) style.set_corner_radius_all(10) return style func _party_master_command_state_style(pressed: bool, primary: bool = false, disabled: bool = false) -> StyleBoxFlat: var style := StyleBoxFlat.new() if disabled: style.bg_color = Color(0.20, 0.24, 0.23, 0.30) style.border_color = Color(0.35, 0.37, 0.33, 0.32) else: style.bg_color = Color(0.28, 0.52, 0.32, (0.38 if pressed else 0.30) if primary else (0.20 if pressed else 0.12)) style.border_color = Color(0.92, 0.76, 0.34, 0.96 if primary else 0.88) style.set_border_width_all(4 if primary else 2) style.set_corner_radius_all(6) return style 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 _title_master_button(node_name: String, title: String, detail: String, callback: Callable, rect: Rect2, kind: String) -> Button: var button := Button.new() button.name = node_name button.flat = true button.clip_contents = true button.focus_mode = Control.FOCUS_ALL button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button.tooltip_text = title button.add_theme_stylebox_override("normal", StyleBoxEmpty.new()) button.add_theme_stylebox_override("hover", _flat_style(Color(1.0, 0.91, 0.65, 0.12), Color(0, 0, 0, 0), 0, 5)) button.add_theme_stylebox_override("pressed", _flat_style(Color(0.20, 0.48, 0.65, 0.17), Color(0, 0, 0, 0), 0, 5)) button.add_theme_stylebox_override("focus", _flat_style(Color(0.20, 0.58, 0.78, 0.13), Color(0, 0, 0, 0), 0, 5)) button.add_theme_stylebox_override("hover_pressed", _flat_style(Color(0.20, 0.48, 0.65, 0.21), Color(0, 0, 0, 0), 0, 5)) button.add_theme_stylebox_override("disabled", _flat_style(Color(0.04, 0.08, 0.10, 0.30), Color(0.24, 0.27, 0.28, 0.55), 1, 5)) _set_normalized_rect(button, rect) button.set_meta("title_master_kind", kind) var label := _label(title, _scaled(15 if kind == "primary" else 12 if kind == "secondary" else 9), Color("#263a43")) label.name = "ButtonLabel" _set_normalized_rect(label, Rect2(0.06, 0.16 if kind != "utility" else 0.08, 0.88, 0.46 if kind != "utility" else 0.84)) 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 button.add_child(label) var detail_label := _label(detail, _scaled(7), Color("#6a4b2d")) detail_label.name = "DetailLabel" _set_normalized_rect(detail_label, Rect2(0.08, 0.52, 0.84, 0.24)) detail_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER detail_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER detail_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS detail_label.mouse_filter = Control.MOUSE_FILTER_IGNORE detail_label.visible = kind != "utility" and not detail.is_empty() button.add_child(detail_label) button.pressed.connect(callback, CONNECT_DEFERRED) return button func _set_title_master_button_disabled(button: Button, disabled: bool) -> void: button.disabled = disabled button.mouse_default_cursor_shape = Control.CURSOR_FORBIDDEN if disabled else Control.CURSOR_POINTING_HAND var title := button.get_node_or_null("ButtonLabel") as Label var detail := button.get_node_or_null("DetailLabel") as Label if title != null: title.add_theme_color_override("font_color", Color("#667078") if disabled else Color("#263a43")) if detail != null: detail.add_theme_color_override("font_color", Color("#77736c") if disabled else Color("#6a4b2d")) 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 _guild_text_size(design_size: int) -> int: var canvas_height := guild_master_design_canvas.size.y if is_instance_valid(guild_master_design_canvas) else size.y var height_ratio := clampf(canvas_height / 360.0, 1.0, 2.0) var canvas_scale := lerpf(1.0, height_ratio, 0.35) var text_scale := float(setup_draft.get("text_scale", settings.get("text_scale", 1.0))) return maxi(7, int(round(float(design_size) * canvas_scale * text_scale))) 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